@kody-ade/kody-engine 0.2.22 → 0.2.26
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/README.md +3 -1
- package/dist/bin/kody2.js +694 -302
- package/dist/executables/fix/profile.json +3 -0
- package/dist/executables/fix/prompt.md +14 -7
- package/dist/executables/types.ts +2 -0
- package/package.json +15 -14
- package/templates/kody2.yml +48 -15
package/dist/bin/kody2.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.26",
|
|
7
7
|
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -49,18 +49,82 @@ var package_default = {
|
|
|
49
49
|
bugs: "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
// src/
|
|
53
|
-
import
|
|
54
|
-
import * as
|
|
52
|
+
// src/chat-cli.ts
|
|
53
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
54
|
+
import * as fs19 from "fs";
|
|
55
|
+
import * as path16 from "path";
|
|
56
|
+
|
|
57
|
+
// src/chat/events.ts
|
|
58
|
+
import * as fs from "fs";
|
|
59
|
+
import * as path from "path";
|
|
60
|
+
function eventsFilePath(cwd, sessionId) {
|
|
61
|
+
return path.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
|
|
62
|
+
}
|
|
63
|
+
var FileSink = class {
|
|
64
|
+
constructor(file) {
|
|
65
|
+
this.file = file;
|
|
66
|
+
}
|
|
67
|
+
file;
|
|
68
|
+
async emit(event) {
|
|
69
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
70
|
+
fs.appendFileSync(this.file, `${JSON.stringify(event)}
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var HttpSink = class {
|
|
75
|
+
constructor(baseUrl, sessionId, logger = {
|
|
76
|
+
warn: (m) => process.stderr.write(`[kody2:chat] ${m}
|
|
77
|
+
`)
|
|
78
|
+
}) {
|
|
79
|
+
this.baseUrl = baseUrl;
|
|
80
|
+
this.sessionId = sessionId;
|
|
81
|
+
this.logger = logger;
|
|
82
|
+
}
|
|
83
|
+
baseUrl;
|
|
84
|
+
sessionId;
|
|
85
|
+
logger;
|
|
86
|
+
async emit(event) {
|
|
87
|
+
const url = withSessionParam(this.baseUrl, this.sessionId);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
body: JSON.stringify(event),
|
|
93
|
+
signal: AbortSignal.timeout(5e3)
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var TeeSink = class {
|
|
104
|
+
constructor(sinks) {
|
|
105
|
+
this.sinks = sinks;
|
|
106
|
+
}
|
|
107
|
+
sinks;
|
|
108
|
+
async emit(event) {
|
|
109
|
+
await Promise.all(this.sinks.map((s) => s.emit(event)));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function withSessionParam(baseUrl, sessionId) {
|
|
113
|
+
const joiner = baseUrl.includes("?") ? "&" : "?";
|
|
114
|
+
return `${baseUrl}${joiner}sessionId=${encodeURIComponent(sessionId)}`;
|
|
115
|
+
}
|
|
116
|
+
function makeRunId(sessionId, suffix) {
|
|
117
|
+
return `chat-${sessionId}-${suffix}`;
|
|
118
|
+
}
|
|
55
119
|
|
|
56
120
|
// src/agent.ts
|
|
57
|
-
import * as
|
|
58
|
-
import * as
|
|
121
|
+
import * as fs3 from "fs";
|
|
122
|
+
import * as path3 from "path";
|
|
59
123
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
60
124
|
|
|
61
125
|
// src/config.ts
|
|
62
|
-
import * as
|
|
63
|
-
import * as
|
|
126
|
+
import * as fs2 from "fs";
|
|
127
|
+
import * as path2 from "path";
|
|
64
128
|
var LITELLM_DEFAULT_PORT = 4e3;
|
|
65
129
|
var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
66
130
|
function parseProviderModel(s) {
|
|
@@ -78,13 +142,13 @@ function needsLitellmProxy(model) {
|
|
|
78
142
|
return model.provider !== "claude" && model.provider !== "anthropic";
|
|
79
143
|
}
|
|
80
144
|
function loadConfig(projectDir = process.cwd()) {
|
|
81
|
-
const configPath =
|
|
82
|
-
if (!
|
|
145
|
+
const configPath = path2.join(projectDir, "kody.config.json");
|
|
146
|
+
if (!fs2.existsSync(configPath)) {
|
|
83
147
|
throw new Error(`kody.config.json not found at ${configPath}`);
|
|
84
148
|
}
|
|
85
149
|
let raw;
|
|
86
150
|
try {
|
|
87
|
-
raw = JSON.parse(
|
|
151
|
+
raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
88
152
|
} catch (err) {
|
|
89
153
|
const msg = err instanceof Error ? err.message : String(err);
|
|
90
154
|
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
@@ -261,10 +325,10 @@ function formatBytes(bytes) {
|
|
|
261
325
|
// src/agent.ts
|
|
262
326
|
var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
|
|
263
327
|
async function runAgent(opts) {
|
|
264
|
-
const ndjsonDir = opts.ndjsonDir ??
|
|
265
|
-
|
|
266
|
-
const ndjsonPath =
|
|
267
|
-
const fullLog =
|
|
328
|
+
const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody2");
|
|
329
|
+
fs3.mkdirSync(ndjsonDir, { recursive: true });
|
|
330
|
+
const ndjsonPath = path3.join(ndjsonDir, "last-run.jsonl");
|
|
331
|
+
const fullLog = fs3.createWriteStream(ndjsonPath, { flags: "w" });
|
|
268
332
|
const env = {
|
|
269
333
|
...process.env,
|
|
270
334
|
SKIP_HOOKS: "1",
|
|
@@ -295,6 +359,9 @@ async function runAgent(opts) {
|
|
|
295
359
|
if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
|
|
296
360
|
queryOptions.maxTurns = opts.maxTurns;
|
|
297
361
|
}
|
|
362
|
+
if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
|
|
363
|
+
queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
|
|
364
|
+
}
|
|
298
365
|
if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
|
|
299
366
|
queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
|
|
300
367
|
}
|
|
@@ -336,11 +403,228 @@ async function runAgent(opts) {
|
|
|
336
403
|
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
337
404
|
}
|
|
338
405
|
|
|
406
|
+
// src/chat/session.ts
|
|
407
|
+
import * as fs4 from "fs";
|
|
408
|
+
import * as path4 from "path";
|
|
409
|
+
function sessionFilePath(cwd, sessionId) {
|
|
410
|
+
return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
411
|
+
}
|
|
412
|
+
function readSession(file) {
|
|
413
|
+
if (!fs4.existsSync(file)) return [];
|
|
414
|
+
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
415
|
+
if (!raw) return [];
|
|
416
|
+
const turns = [];
|
|
417
|
+
for (const line of raw.split("\n")) {
|
|
418
|
+
if (!line.trim()) continue;
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(line);
|
|
421
|
+
if (parsed.role !== "user" && parsed.role !== "assistant") continue;
|
|
422
|
+
if (typeof parsed.content !== "string") continue;
|
|
423
|
+
turns.push(parsed);
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return turns;
|
|
428
|
+
}
|
|
429
|
+
function appendTurn(file, turn) {
|
|
430
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
431
|
+
const line = JSON.stringify({
|
|
432
|
+
role: turn.role,
|
|
433
|
+
content: turn.content,
|
|
434
|
+
timestamp: turn.timestamp,
|
|
435
|
+
toolCalls: turn.toolCalls ?? []
|
|
436
|
+
});
|
|
437
|
+
fs4.appendFileSync(file, `${line}
|
|
438
|
+
`);
|
|
439
|
+
}
|
|
440
|
+
function seedInitialMessage(file, message) {
|
|
441
|
+
if (!message.trim()) return false;
|
|
442
|
+
const turns = readSession(file);
|
|
443
|
+
const lastUser = [...turns].reverse().find((t) => t.role === "user");
|
|
444
|
+
if (lastUser && lastUser.content === message) return false;
|
|
445
|
+
appendTurn(file, {
|
|
446
|
+
role: "user",
|
|
447
|
+
content: message,
|
|
448
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
449
|
+
});
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/chat/loop.ts
|
|
454
|
+
var CHAT_SYSTEM_PROMPT = [
|
|
455
|
+
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
|
|
456
|
+
"latest message using the full conversation below as context. Keep replies focused,",
|
|
457
|
+
"technical when appropriate, and formatted in Markdown. Use the available tools to",
|
|
458
|
+
"read repository code or execute small checks when it helps you answer \u2014 otherwise",
|
|
459
|
+
"reply directly. Do not invent file paths, commit SHAs, or command output."
|
|
460
|
+
].join("\n");
|
|
461
|
+
async function runChatTurn(opts) {
|
|
462
|
+
const turns = readSession(opts.sessionFile);
|
|
463
|
+
if (turns.length === 0) {
|
|
464
|
+
const error = "session file is empty \u2014 nothing to reply to";
|
|
465
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
466
|
+
return { exitCode: 64, error };
|
|
467
|
+
}
|
|
468
|
+
const lastTurn = turns[turns.length - 1];
|
|
469
|
+
if (lastTurn.role !== "user") {
|
|
470
|
+
const error = "last turn is not a user message \u2014 assistant already replied";
|
|
471
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
472
|
+
return { exitCode: 64, error };
|
|
473
|
+
}
|
|
474
|
+
const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
|
|
475
|
+
const invoke = opts.invokeAgent ?? ((p) => runAgent({
|
|
476
|
+
prompt: p,
|
|
477
|
+
model: opts.model,
|
|
478
|
+
cwd: opts.cwd,
|
|
479
|
+
litellmUrl: opts.litellmUrl,
|
|
480
|
+
verbose: opts.verbose,
|
|
481
|
+
quiet: opts.quiet
|
|
482
|
+
}));
|
|
483
|
+
let result;
|
|
484
|
+
try {
|
|
485
|
+
result = await invoke(prompt);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
488
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
489
|
+
return { exitCode: 99, error };
|
|
490
|
+
}
|
|
491
|
+
if (result.outcome !== "completed") {
|
|
492
|
+
const error = result.error ?? "agent did not complete";
|
|
493
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
494
|
+
return { exitCode: 99, error };
|
|
495
|
+
}
|
|
496
|
+
const reply = result.finalText.trim();
|
|
497
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
498
|
+
appendTurn(opts.sessionFile, {
|
|
499
|
+
role: "assistant",
|
|
500
|
+
content: reply,
|
|
501
|
+
timestamp: now
|
|
502
|
+
});
|
|
503
|
+
await emit(opts.sink, "chat.message", opts.sessionId, "message", {
|
|
504
|
+
sessionId: opts.sessionId,
|
|
505
|
+
role: "assistant",
|
|
506
|
+
content: reply,
|
|
507
|
+
timestamp: now
|
|
508
|
+
});
|
|
509
|
+
await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
|
|
510
|
+
return { exitCode: 0, reply };
|
|
511
|
+
}
|
|
512
|
+
function buildPrompt(turns, systemPrompt) {
|
|
513
|
+
const header = `System: ${systemPrompt}`;
|
|
514
|
+
const body = turns.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`).join("\n\n");
|
|
515
|
+
return `${header}
|
|
516
|
+
|
|
517
|
+
${body}
|
|
518
|
+
|
|
519
|
+
Assistant:`;
|
|
520
|
+
}
|
|
521
|
+
async function emit(sink, type, sessionId, suffix, payload) {
|
|
522
|
+
await sink.emit({
|
|
523
|
+
event: type,
|
|
524
|
+
payload,
|
|
525
|
+
runId: makeRunId(sessionId, suffix),
|
|
526
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/kody2-cli.ts
|
|
531
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
532
|
+
import * as fs18 from "fs";
|
|
533
|
+
import * as path15 from "path";
|
|
534
|
+
|
|
535
|
+
// src/dispatch.ts
|
|
536
|
+
import * as fs5 from "fs";
|
|
537
|
+
function autoDispatch(opts) {
|
|
538
|
+
const explicit = opts?.explicit;
|
|
539
|
+
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
540
|
+
return {
|
|
541
|
+
executable: "run",
|
|
542
|
+
cliArgs: { issue: explicit.issueNumber },
|
|
543
|
+
target: explicit.issueNumber
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
547
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
548
|
+
if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
|
|
549
|
+
let event = {};
|
|
550
|
+
try {
|
|
551
|
+
event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if (eventName === "workflow_dispatch") {
|
|
556
|
+
const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
|
|
557
|
+
if (!Number.isNaN(n) && n > 0) {
|
|
558
|
+
return { executable: "run", cliArgs: { issue: n }, target: n };
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
if (eventName !== "issue_comment") return null;
|
|
563
|
+
const body = String(event.comment?.body ?? "").toLowerCase();
|
|
564
|
+
const targetNum = Number(event.issue?.number ?? 0);
|
|
565
|
+
const isPr = !!event.issue?.pull_request;
|
|
566
|
+
if (!targetNum) return null;
|
|
567
|
+
const afterTag = extractAfterTag(body);
|
|
568
|
+
if (isPr) {
|
|
569
|
+
if (/\bfix-ci\b/.test(afterTag)) {
|
|
570
|
+
return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
|
|
571
|
+
}
|
|
572
|
+
if (/\bresolve\b/.test(afterTag)) {
|
|
573
|
+
return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
|
|
574
|
+
}
|
|
575
|
+
if (/\breview\b/.test(afterTag)) {
|
|
576
|
+
return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
|
|
577
|
+
}
|
|
578
|
+
if (/\bsync\b/.test(afterTag)) {
|
|
579
|
+
return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
|
|
580
|
+
}
|
|
581
|
+
const feedback = extractFeedback(afterTag);
|
|
582
|
+
return {
|
|
583
|
+
executable: "fix",
|
|
584
|
+
cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
|
|
585
|
+
target: targetNum
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const sub = extractSubcommand(afterTag);
|
|
589
|
+
const defaultExec = opts?.config?.defaultExecutable ?? "run";
|
|
590
|
+
if (!sub) {
|
|
591
|
+
return asDispatch(defaultExec, targetNum);
|
|
592
|
+
}
|
|
593
|
+
if (sub === "orchestrate" || sub === "orchestrator") {
|
|
594
|
+
return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
|
|
595
|
+
}
|
|
596
|
+
if (sub === "build") {
|
|
597
|
+
return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
|
|
598
|
+
}
|
|
599
|
+
return asDispatch(sub, targetNum);
|
|
600
|
+
}
|
|
601
|
+
function asDispatch(executable, target) {
|
|
602
|
+
return { executable, cliArgs: { issue: target }, target };
|
|
603
|
+
}
|
|
604
|
+
function extractAfterTag(body) {
|
|
605
|
+
const idx = body.indexOf("@kody2");
|
|
606
|
+
if (idx === -1) return body;
|
|
607
|
+
return body.slice(idx + "@kody2".length).trim();
|
|
608
|
+
}
|
|
609
|
+
function extractSubcommand(afterTag) {
|
|
610
|
+
const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
|
|
611
|
+
if (!match) return null;
|
|
612
|
+
return match[1];
|
|
613
|
+
}
|
|
614
|
+
function extractFeedback(afterTag) {
|
|
615
|
+
const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
|
|
616
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/executor.ts
|
|
620
|
+
import * as fs17 from "fs";
|
|
621
|
+
import * as path14 from "path";
|
|
622
|
+
|
|
339
623
|
// src/litellm.ts
|
|
340
624
|
import { execFileSync, spawn } from "child_process";
|
|
341
|
-
import * as
|
|
625
|
+
import * as fs6 from "fs";
|
|
342
626
|
import * as os from "os";
|
|
343
|
-
import * as
|
|
627
|
+
import * as path5 from "path";
|
|
344
628
|
async function checkLitellmHealth(url) {
|
|
345
629
|
try {
|
|
346
630
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -380,20 +664,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
380
664
|
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
381
665
|
}
|
|
382
666
|
}
|
|
383
|
-
const configPath =
|
|
384
|
-
|
|
667
|
+
const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
668
|
+
fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
385
669
|
const portMatch = url.match(/:(\d+)/);
|
|
386
670
|
const port = portMatch ? portMatch[1] : "4000";
|
|
387
671
|
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
388
672
|
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
389
|
-
const logPath =
|
|
390
|
-
const outFd =
|
|
673
|
+
const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
674
|
+
const outFd = fs6.openSync(logPath, "w");
|
|
391
675
|
const child = spawn(cmd, args, {
|
|
392
676
|
stdio: ["ignore", outFd, outFd],
|
|
393
677
|
detached: true,
|
|
394
678
|
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
395
679
|
});
|
|
396
|
-
|
|
680
|
+
fs6.closeSync(outFd);
|
|
397
681
|
for (let i = 0; i < 30; i++) {
|
|
398
682
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
399
683
|
if (await checkLitellmHealth(url)) {
|
|
@@ -410,7 +694,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
410
694
|
}
|
|
411
695
|
let logTail = "";
|
|
412
696
|
try {
|
|
413
|
-
logTail =
|
|
697
|
+
logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
414
698
|
} catch {
|
|
415
699
|
}
|
|
416
700
|
try {
|
|
@@ -421,10 +705,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
421
705
|
${logTail}`);
|
|
422
706
|
}
|
|
423
707
|
function readDotenvApiKeys(projectDir) {
|
|
424
|
-
const dotenvPath =
|
|
425
|
-
if (!
|
|
708
|
+
const dotenvPath = path5.join(projectDir, ".env");
|
|
709
|
+
if (!fs6.existsSync(dotenvPath)) return {};
|
|
426
710
|
const result = {};
|
|
427
|
-
for (const rawLine of
|
|
711
|
+
for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
428
712
|
const line = rawLine.trim();
|
|
429
713
|
if (!line || line.startsWith("#")) continue;
|
|
430
714
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
@@ -447,8 +731,8 @@ function stripBlockingEnv(env) {
|
|
|
447
731
|
}
|
|
448
732
|
|
|
449
733
|
// src/profile.ts
|
|
450
|
-
import * as
|
|
451
|
-
import * as
|
|
734
|
+
import * as fs7 from "fs";
|
|
735
|
+
import * as path6 from "path";
|
|
452
736
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
453
737
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
454
738
|
var ProfileError = class extends Error {
|
|
@@ -461,12 +745,12 @@ var ProfileError = class extends Error {
|
|
|
461
745
|
profilePath;
|
|
462
746
|
};
|
|
463
747
|
function loadProfile(profilePath) {
|
|
464
|
-
if (!
|
|
748
|
+
if (!fs7.existsSync(profilePath)) {
|
|
465
749
|
throw new ProfileError(profilePath, "file not found");
|
|
466
750
|
}
|
|
467
751
|
let raw;
|
|
468
752
|
try {
|
|
469
|
-
raw = JSON.parse(
|
|
753
|
+
raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
|
|
470
754
|
} catch (err) {
|
|
471
755
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
472
756
|
}
|
|
@@ -490,7 +774,7 @@ function loadProfile(profilePath) {
|
|
|
490
774
|
outputContract: r.outputContract,
|
|
491
775
|
inputArtifacts: parseInputArtifacts(profilePath, r.input),
|
|
492
776
|
outputArtifacts: parseOutputArtifacts(profilePath, r.output),
|
|
493
|
-
dir:
|
|
777
|
+
dir: path6.dirname(profilePath)
|
|
494
778
|
};
|
|
495
779
|
return profile;
|
|
496
780
|
}
|
|
@@ -556,6 +840,7 @@ function parseClaudeCode(p, raw) {
|
|
|
556
840
|
model: typeof r.model === "string" ? r.model : "inherit",
|
|
557
841
|
permissionMode,
|
|
558
842
|
maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
|
|
843
|
+
maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
|
|
559
844
|
systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
|
|
560
845
|
tools,
|
|
561
846
|
hooks: Array.isArray(r.hooks) ? r.hooks : [],
|
|
@@ -669,21 +954,21 @@ function parseScriptList(p, key, raw) {
|
|
|
669
954
|
}
|
|
670
955
|
|
|
671
956
|
// src/scripts/buildSyntheticPlugin.ts
|
|
672
|
-
import * as
|
|
957
|
+
import * as fs8 from "fs";
|
|
673
958
|
import * as os2 from "os";
|
|
674
|
-
import * as
|
|
959
|
+
import * as path7 from "path";
|
|
675
960
|
function getPluginsCatalogRoot() {
|
|
676
|
-
const here =
|
|
961
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
677
962
|
const candidates = [
|
|
678
|
-
|
|
963
|
+
path7.join(here, "..", "plugins"),
|
|
679
964
|
// dev: src/scripts → src/plugins
|
|
680
|
-
|
|
965
|
+
path7.join(here, "..", "..", "plugins"),
|
|
681
966
|
// built: dist/scripts → dist/plugins
|
|
682
|
-
|
|
967
|
+
path7.join(here, "..", "..", "src", "plugins")
|
|
683
968
|
// fallback
|
|
684
969
|
];
|
|
685
970
|
for (const c of candidates) {
|
|
686
|
-
if (
|
|
971
|
+
if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
|
|
687
972
|
}
|
|
688
973
|
return candidates[0];
|
|
689
974
|
}
|
|
@@ -693,50 +978,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
693
978
|
if (!needsSynthetic) return;
|
|
694
979
|
const catalog = getPluginsCatalogRoot();
|
|
695
980
|
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
696
|
-
const root =
|
|
697
|
-
|
|
981
|
+
const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
|
|
982
|
+
fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
|
|
698
983
|
if (cc.skills.length > 0) {
|
|
699
|
-
const dst =
|
|
700
|
-
|
|
984
|
+
const dst = path7.join(root, "skills");
|
|
985
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
701
986
|
for (const name of cc.skills) {
|
|
702
|
-
const src =
|
|
703
|
-
if (!
|
|
704
|
-
copyDir(src,
|
|
987
|
+
const src = path7.join(catalog, "skills", name);
|
|
988
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
|
|
989
|
+
copyDir(src, path7.join(dst, name));
|
|
705
990
|
}
|
|
706
991
|
}
|
|
707
992
|
if (cc.commands.length > 0) {
|
|
708
|
-
const dst =
|
|
709
|
-
|
|
993
|
+
const dst = path7.join(root, "commands");
|
|
994
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
710
995
|
for (const name of cc.commands) {
|
|
711
|
-
const src =
|
|
712
|
-
if (!
|
|
713
|
-
|
|
996
|
+
const src = path7.join(catalog, "commands", `${name}.md`);
|
|
997
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
|
|
998
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
714
999
|
}
|
|
715
1000
|
}
|
|
716
1001
|
if (cc.subagents.length > 0) {
|
|
717
|
-
const dst =
|
|
718
|
-
|
|
1002
|
+
const dst = path7.join(root, "agents");
|
|
1003
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
719
1004
|
for (const name of cc.subagents) {
|
|
720
|
-
const src =
|
|
721
|
-
if (!
|
|
722
|
-
|
|
1005
|
+
const src = path7.join(catalog, "agents", `${name}.md`);
|
|
1006
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
|
|
1007
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
723
1008
|
}
|
|
724
1009
|
}
|
|
725
1010
|
if (cc.hooks.length > 0) {
|
|
726
|
-
const dst =
|
|
727
|
-
|
|
1011
|
+
const dst = path7.join(root, "hooks");
|
|
1012
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
728
1013
|
const merged = { hooks: {} };
|
|
729
1014
|
for (const name of cc.hooks) {
|
|
730
|
-
const src =
|
|
731
|
-
if (!
|
|
732
|
-
const parsed = JSON.parse(
|
|
1015
|
+
const src = path7.join(catalog, "hooks", `${name}.json`);
|
|
1016
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
|
|
1017
|
+
const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
|
|
733
1018
|
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
734
1019
|
if (!Array.isArray(entries)) continue;
|
|
735
1020
|
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
736
1021
|
merged.hooks[event].push(...entries);
|
|
737
1022
|
}
|
|
738
1023
|
}
|
|
739
|
-
|
|
1024
|
+
fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
740
1025
|
`);
|
|
741
1026
|
}
|
|
742
1027
|
const manifest = {
|
|
@@ -747,17 +1032,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
747
1032
|
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
748
1033
|
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
749
1034
|
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
750
|
-
|
|
1035
|
+
fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
751
1036
|
`);
|
|
752
1037
|
ctx.data.syntheticPluginPath = root;
|
|
753
1038
|
};
|
|
754
1039
|
function copyDir(src, dst) {
|
|
755
|
-
|
|
756
|
-
for (const ent of
|
|
757
|
-
const s =
|
|
758
|
-
const d =
|
|
1040
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1041
|
+
for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
|
|
1042
|
+
const s = path7.join(src, ent.name);
|
|
1043
|
+
const d = path7.join(dst, ent.name);
|
|
759
1044
|
if (ent.isDirectory()) copyDir(s, d);
|
|
760
|
-
else if (ent.isFile())
|
|
1045
|
+
else if (ent.isFile()) fs8.copyFileSync(s, d);
|
|
761
1046
|
}
|
|
762
1047
|
}
|
|
763
1048
|
|
|
@@ -823,18 +1108,18 @@ function formatMissesForFeedback(misses) {
|
|
|
823
1108
|
}
|
|
824
1109
|
|
|
825
1110
|
// src/prompt.ts
|
|
826
|
-
import * as
|
|
827
|
-
import * as
|
|
1111
|
+
import * as fs9 from "fs";
|
|
1112
|
+
import * as path8 from "path";
|
|
828
1113
|
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
829
1114
|
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
830
1115
|
function loadProjectConventions(projectDir) {
|
|
831
1116
|
const out = [];
|
|
832
1117
|
for (const rel of CONVENTION_FILES) {
|
|
833
|
-
const abs =
|
|
834
|
-
if (!
|
|
1118
|
+
const abs = path8.join(projectDir, rel);
|
|
1119
|
+
if (!fs9.existsSync(abs)) continue;
|
|
835
1120
|
let content;
|
|
836
1121
|
try {
|
|
837
|
-
content =
|
|
1122
|
+
content = fs9.readFileSync(abs, "utf-8");
|
|
838
1123
|
} catch {
|
|
839
1124
|
continue;
|
|
840
1125
|
}
|
|
@@ -848,23 +1133,49 @@ function loadProjectConventions(projectDir) {
|
|
|
848
1133
|
}
|
|
849
1134
|
function parseAgentResult(finalText) {
|
|
850
1135
|
const text = (finalText || "").trim();
|
|
851
|
-
if (!text)
|
|
1136
|
+
if (!text)
|
|
1137
|
+
return {
|
|
1138
|
+
done: false,
|
|
1139
|
+
commitMessage: "",
|
|
1140
|
+
prSummary: "",
|
|
1141
|
+
feedbackActions: "",
|
|
1142
|
+
failureReason: "agent produced no final message"
|
|
1143
|
+
};
|
|
852
1144
|
const failedMatch = text.match(/(?:^|\n)\s*FAILED\s*:\s*(.+?)\s*$/s);
|
|
853
1145
|
if (failedMatch) {
|
|
854
|
-
return { done: false, commitMessage: "", prSummary: "", failureReason: failedMatch[1].trim() };
|
|
1146
|
+
return { done: false, commitMessage: "", prSummary: "", feedbackActions: "", failureReason: failedMatch[1].trim() };
|
|
855
1147
|
}
|
|
856
1148
|
if (!/(^|\n)\s*DONE\b/i.test(text)) {
|
|
857
|
-
return {
|
|
1149
|
+
return {
|
|
1150
|
+
done: false,
|
|
1151
|
+
commitMessage: "",
|
|
1152
|
+
prSummary: "",
|
|
1153
|
+
feedbackActions: "",
|
|
1154
|
+
failureReason: "no DONE or FAILED marker in agent output"
|
|
1155
|
+
};
|
|
858
1156
|
}
|
|
859
1157
|
const commitMatch = text.match(/^[ \t]*COMMIT_MSG\s*:\s*(.+)$/im);
|
|
860
1158
|
const commitMessage = commitMatch ? commitMatch[1].trim() : "";
|
|
1159
|
+
const feedbackActions = extractBlock(
|
|
1160
|
+
text,
|
|
1161
|
+
/(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
|
|
1162
|
+
/(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY)\s*:/i
|
|
1163
|
+
);
|
|
861
1164
|
const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
|
|
862
1165
|
let prSummary = "";
|
|
863
1166
|
if (summaryStart !== -1) {
|
|
864
1167
|
const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
|
|
865
1168
|
prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
|
|
866
1169
|
}
|
|
867
|
-
return { done: true, commitMessage, prSummary, failureReason: "" };
|
|
1170
|
+
return { done: true, commitMessage, prSummary, feedbackActions, failureReason: "" };
|
|
1171
|
+
}
|
|
1172
|
+
function extractBlock(text, startMarker, endMarker) {
|
|
1173
|
+
const startIdx = text.search(startMarker);
|
|
1174
|
+
if (startIdx === -1) return "";
|
|
1175
|
+
const afterStart = text.slice(startIdx).replace(startMarker, "");
|
|
1176
|
+
const endIdx = afterStart.search(endMarker);
|
|
1177
|
+
const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
|
|
1178
|
+
return body.replace(/\n\s*```\s*$/g, "").trim();
|
|
868
1179
|
}
|
|
869
1180
|
|
|
870
1181
|
// src/scripts/checkCoverageWithRetry.ts
|
|
@@ -911,8 +1222,8 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
911
1222
|
|
|
912
1223
|
// src/commit.ts
|
|
913
1224
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
914
|
-
import * as
|
|
915
|
-
import * as
|
|
1225
|
+
import * as fs10 from "fs";
|
|
1226
|
+
import * as path9 from "path";
|
|
916
1227
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
917
1228
|
".kody/",
|
|
918
1229
|
".kody-engine/",
|
|
@@ -967,18 +1278,18 @@ function tryGit(args, cwd) {
|
|
|
967
1278
|
}
|
|
968
1279
|
function abortUnfinishedGitOps(cwd) {
|
|
969
1280
|
const aborted = [];
|
|
970
|
-
const gitDir =
|
|
971
|
-
if (!
|
|
972
|
-
if (
|
|
1281
|
+
const gitDir = path9.join(cwd ?? process.cwd(), ".git");
|
|
1282
|
+
if (!fs10.existsSync(gitDir)) return aborted;
|
|
1283
|
+
if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
|
|
973
1284
|
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
974
1285
|
}
|
|
975
|
-
if (
|
|
1286
|
+
if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
976
1287
|
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
977
1288
|
}
|
|
978
|
-
if (
|
|
1289
|
+
if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
|
|
979
1290
|
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
980
1291
|
}
|
|
981
|
-
if (
|
|
1292
|
+
if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
|
|
982
1293
|
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
983
1294
|
}
|
|
984
1295
|
try {
|
|
@@ -1020,7 +1331,7 @@ function normalizeCommitMessage(raw) {
|
|
|
1020
1331
|
function commitAndPush(branch, agentMessage, cwd) {
|
|
1021
1332
|
const allChanged = listChangedFiles(cwd);
|
|
1022
1333
|
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
1023
|
-
const mergeHeadExists =
|
|
1334
|
+
const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
1024
1335
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
1025
1336
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
1026
1337
|
}
|
|
@@ -1110,20 +1421,20 @@ function defaultCommitMessage(mode, data) {
|
|
|
1110
1421
|
}
|
|
1111
1422
|
|
|
1112
1423
|
// src/scripts/composePrompt.ts
|
|
1113
|
-
import * as
|
|
1114
|
-
import * as
|
|
1424
|
+
import * as fs11 from "fs";
|
|
1425
|
+
import * as path10 from "path";
|
|
1115
1426
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
1116
1427
|
var composePrompt = async (ctx, profile) => {
|
|
1117
1428
|
const explicit = ctx.data.promptTemplate;
|
|
1118
1429
|
const mode = ctx.args.mode;
|
|
1119
1430
|
const candidates = [
|
|
1120
|
-
explicit ?
|
|
1121
|
-
mode ?
|
|
1122
|
-
|
|
1431
|
+
explicit ? path10.join(profile.dir, explicit) : null,
|
|
1432
|
+
mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
1433
|
+
path10.join(profile.dir, "prompt.md")
|
|
1123
1434
|
].filter(Boolean);
|
|
1124
1435
|
let templatePath = "";
|
|
1125
1436
|
for (const c of candidates) {
|
|
1126
|
-
if (
|
|
1437
|
+
if (fs11.existsSync(c)) {
|
|
1127
1438
|
templatePath = c;
|
|
1128
1439
|
break;
|
|
1129
1440
|
}
|
|
@@ -1131,7 +1442,7 @@ var composePrompt = async (ctx, profile) => {
|
|
|
1131
1442
|
if (!templatePath) {
|
|
1132
1443
|
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
1133
1444
|
}
|
|
1134
|
-
const template =
|
|
1445
|
+
const template = fs11.readFileSync(templatePath, "utf-8");
|
|
1135
1446
|
const tokens = {
|
|
1136
1447
|
...stringifyAll(ctx.args, "args."),
|
|
1137
1448
|
...stringifyAll(ctx.data, ""),
|
|
@@ -1603,7 +1914,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
1603
1914
|
|
|
1604
1915
|
// src/gha.ts
|
|
1605
1916
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1606
|
-
import * as
|
|
1917
|
+
import * as fs12 from "fs";
|
|
1607
1918
|
function getRunUrl() {
|
|
1608
1919
|
const server = process.env.GITHUB_SERVER_URL;
|
|
1609
1920
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -1614,10 +1925,10 @@ function getRunUrl() {
|
|
|
1614
1925
|
function reactToTriggerComment(cwd) {
|
|
1615
1926
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
1616
1927
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1617
|
-
if (!eventPath || !
|
|
1928
|
+
if (!eventPath || !fs12.existsSync(eventPath)) return;
|
|
1618
1929
|
let event = null;
|
|
1619
1930
|
try {
|
|
1620
|
-
event = JSON.parse(
|
|
1931
|
+
event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
|
|
1621
1932
|
} catch {
|
|
1622
1933
|
return;
|
|
1623
1934
|
}
|
|
@@ -1843,35 +2154,35 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
1843
2154
|
|
|
1844
2155
|
// src/scripts/initFlow.ts
|
|
1845
2156
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
1846
|
-
import * as
|
|
1847
|
-
import * as
|
|
2157
|
+
import * as fs14 from "fs";
|
|
2158
|
+
import * as path12 from "path";
|
|
1848
2159
|
|
|
1849
2160
|
// src/registry.ts
|
|
1850
|
-
import * as
|
|
1851
|
-
import * as
|
|
2161
|
+
import * as fs13 from "fs";
|
|
2162
|
+
import * as path11 from "path";
|
|
1852
2163
|
function getExecutablesRoot() {
|
|
1853
|
-
const here =
|
|
2164
|
+
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
1854
2165
|
const candidates = [
|
|
1855
|
-
|
|
2166
|
+
path11.join(here, "executables"),
|
|
1856
2167
|
// dev: src/
|
|
1857
|
-
|
|
2168
|
+
path11.join(here, "..", "executables"),
|
|
1858
2169
|
// built: dist/bin → dist/executables
|
|
1859
|
-
|
|
2170
|
+
path11.join(here, "..", "src", "executables")
|
|
1860
2171
|
// fallback
|
|
1861
2172
|
];
|
|
1862
2173
|
for (const c of candidates) {
|
|
1863
|
-
if (
|
|
2174
|
+
if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
|
|
1864
2175
|
}
|
|
1865
2176
|
return candidates[0];
|
|
1866
2177
|
}
|
|
1867
2178
|
function listExecutables(root = getExecutablesRoot()) {
|
|
1868
|
-
if (!
|
|
1869
|
-
const entries =
|
|
2179
|
+
if (!fs13.existsSync(root)) return [];
|
|
2180
|
+
const entries = fs13.readdirSync(root, { withFileTypes: true });
|
|
1870
2181
|
const out = [];
|
|
1871
2182
|
for (const ent of entries) {
|
|
1872
2183
|
if (!ent.isDirectory()) continue;
|
|
1873
|
-
const profilePath =
|
|
1874
|
-
if (
|
|
2184
|
+
const profilePath = path11.join(root, ent.name, "profile.json");
|
|
2185
|
+
if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
|
|
1875
2186
|
out.push({ name: ent.name, profilePath });
|
|
1876
2187
|
}
|
|
1877
2188
|
}
|
|
@@ -1879,8 +2190,8 @@ function listExecutables(root = getExecutablesRoot()) {
|
|
|
1879
2190
|
}
|
|
1880
2191
|
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
1881
2192
|
if (!isSafeName(name)) return false;
|
|
1882
|
-
const profilePath =
|
|
1883
|
-
return
|
|
2193
|
+
const profilePath = path11.join(root, name, "profile.json");
|
|
2194
|
+
return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
|
|
1884
2195
|
}
|
|
1885
2196
|
function isSafeName(name) {
|
|
1886
2197
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
@@ -1909,9 +2220,9 @@ function parseGenericFlags(argv) {
|
|
|
1909
2220
|
|
|
1910
2221
|
// src/scripts/initFlow.ts
|
|
1911
2222
|
function detectPackageManager(cwd) {
|
|
1912
|
-
if (
|
|
1913
|
-
if (
|
|
1914
|
-
if (
|
|
2223
|
+
if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2224
|
+
if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
2225
|
+
if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
|
|
1915
2226
|
return "npm";
|
|
1916
2227
|
}
|
|
1917
2228
|
function qualityCommandsFor(pm) {
|
|
@@ -2032,22 +2343,22 @@ function performInit(cwd, force) {
|
|
|
2032
2343
|
const pm = detectPackageManager(cwd);
|
|
2033
2344
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
2034
2345
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
2035
|
-
const configPath =
|
|
2036
|
-
if (
|
|
2346
|
+
const configPath = path12.join(cwd, "kody.config.json");
|
|
2347
|
+
if (fs14.existsSync(configPath) && !force) {
|
|
2037
2348
|
skipped.push("kody.config.json");
|
|
2038
2349
|
} else {
|
|
2039
2350
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
2040
|
-
|
|
2351
|
+
fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
2041
2352
|
`);
|
|
2042
2353
|
wrote.push("kody.config.json");
|
|
2043
2354
|
}
|
|
2044
|
-
const workflowDir =
|
|
2045
|
-
const workflowPath =
|
|
2046
|
-
if (
|
|
2355
|
+
const workflowDir = path12.join(cwd, ".github", "workflows");
|
|
2356
|
+
const workflowPath = path12.join(workflowDir, "kody2.yml");
|
|
2357
|
+
if (fs14.existsSync(workflowPath) && !force) {
|
|
2047
2358
|
skipped.push(".github/workflows/kody2.yml");
|
|
2048
2359
|
} else {
|
|
2049
|
-
|
|
2050
|
-
|
|
2360
|
+
fs14.mkdirSync(workflowDir, { recursive: true });
|
|
2361
|
+
fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
2051
2362
|
wrote.push(".github/workflows/kody2.yml");
|
|
2052
2363
|
}
|
|
2053
2364
|
for (const exe of listExecutables()) {
|
|
@@ -2058,12 +2369,12 @@ function performInit(cwd, force) {
|
|
|
2058
2369
|
continue;
|
|
2059
2370
|
}
|
|
2060
2371
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
2061
|
-
const target =
|
|
2062
|
-
if (
|
|
2372
|
+
const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
2373
|
+
if (fs14.existsSync(target) && !force) {
|
|
2063
2374
|
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2064
2375
|
continue;
|
|
2065
2376
|
}
|
|
2066
|
-
|
|
2377
|
+
fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
2067
2378
|
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2068
2379
|
}
|
|
2069
2380
|
return { wrote, skipped };
|
|
@@ -2277,17 +2588,19 @@ function renderStateComment(state) {
|
|
|
2277
2588
|
lines.push(STATE_BEGIN);
|
|
2278
2589
|
lines.push("");
|
|
2279
2590
|
lines.push("```json");
|
|
2280
|
-
lines.push(
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2591
|
+
lines.push(
|
|
2592
|
+
JSON.stringify(
|
|
2593
|
+
{
|
|
2594
|
+
schemaVersion: state.schemaVersion,
|
|
2595
|
+
core: state.core,
|
|
2596
|
+
artifacts: state.artifacts ?? {},
|
|
2597
|
+
executables: state.executables,
|
|
2598
|
+
history: state.history
|
|
2599
|
+
},
|
|
2600
|
+
null,
|
|
2601
|
+
2
|
|
2602
|
+
)
|
|
2603
|
+
);
|
|
2291
2604
|
lines.push("```");
|
|
2292
2605
|
lines.push("");
|
|
2293
2606
|
lines.push(STATE_END);
|
|
@@ -2337,11 +2650,7 @@ function writeTaskState(target, number, state, cwd) {
|
|
|
2337
2650
|
const existing = findStateComment(target, number, cwd);
|
|
2338
2651
|
try {
|
|
2339
2652
|
if (existing) {
|
|
2340
|
-
gh3(
|
|
2341
|
-
["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
|
|
2342
|
-
body,
|
|
2343
|
-
cwd
|
|
2344
|
-
);
|
|
2653
|
+
gh3(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
|
|
2345
2654
|
} else {
|
|
2346
2655
|
const sub = target === "issue" ? "issue" : "pr";
|
|
2347
2656
|
gh3([sub, "comment", String(number), "--body-file", "-"], body, cwd);
|
|
@@ -2376,6 +2685,7 @@ var parseAgentResult2 = async (ctx, profile, agentResult) => {
|
|
|
2376
2685
|
ctx.data.agentDone = parsed.done;
|
|
2377
2686
|
ctx.data.commitMessage = parsed.commitMessage;
|
|
2378
2687
|
ctx.data.prSummary = parsed.prSummary;
|
|
2688
|
+
ctx.data.feedbackActions = parsed.feedbackActions;
|
|
2379
2689
|
ctx.data.agentFailureReason = parsed.failureReason;
|
|
2380
2690
|
ctx.data.agentOutcome = agentResult.outcome;
|
|
2381
2691
|
ctx.data.agentError = agentResult.error;
|
|
@@ -2545,15 +2855,17 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
|
|
|
2545
2855
|
const verdict = detectVerdict(reviewBody);
|
|
2546
2856
|
ctx.data.reviewVerdict = verdict;
|
|
2547
2857
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
2548
|
-
process.stdout.write(
|
|
2858
|
+
process.stdout.write(
|
|
2859
|
+
`
|
|
2549
2860
|
REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
|
|
2550
|
-
`
|
|
2861
|
+
`
|
|
2862
|
+
);
|
|
2551
2863
|
};
|
|
2552
2864
|
|
|
2553
2865
|
// src/scripts/releaseFlow.ts
|
|
2554
2866
|
import { execFileSync as execFileSync11, spawnSync } from "child_process";
|
|
2555
|
-
import * as
|
|
2556
|
-
import * as
|
|
2867
|
+
import * as fs15 from "fs";
|
|
2868
|
+
import * as path13 from "path";
|
|
2557
2869
|
function bumpVersion(current, bump) {
|
|
2558
2870
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
2559
2871
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -2569,12 +2881,12 @@ function bumpVersion(current, bump) {
|
|
|
2569
2881
|
return `${major}.${minor}.${patch}`;
|
|
2570
2882
|
}
|
|
2571
2883
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
2572
|
-
const abs =
|
|
2573
|
-
if (!
|
|
2574
|
-
const content =
|
|
2884
|
+
const abs = path13.join(cwd, file);
|
|
2885
|
+
if (!fs15.existsSync(abs)) return false;
|
|
2886
|
+
const content = fs15.readFileSync(abs, "utf-8");
|
|
2575
2887
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
2576
2888
|
if (updated === content) return false;
|
|
2577
|
-
|
|
2889
|
+
fs15.writeFileSync(abs, updated);
|
|
2578
2890
|
return true;
|
|
2579
2891
|
}
|
|
2580
2892
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -2622,19 +2934,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
2622
2934
|
return parts.join("\n");
|
|
2623
2935
|
}
|
|
2624
2936
|
function prependChangelog(cwd, entry) {
|
|
2625
|
-
const p =
|
|
2937
|
+
const p = path13.join(cwd, "CHANGELOG.md");
|
|
2626
2938
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
2627
|
-
if (
|
|
2628
|
-
const prior =
|
|
2939
|
+
if (fs15.existsSync(p)) {
|
|
2940
|
+
const prior = fs15.readFileSync(p, "utf-8");
|
|
2629
2941
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
2630
2942
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
2631
|
-
|
|
2943
|
+
fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
2632
2944
|
${entry}${prior.slice(idx + 1)}`);
|
|
2633
2945
|
} else {
|
|
2634
|
-
|
|
2946
|
+
fs15.writeFileSync(p, `${header}${entry}${prior}`);
|
|
2635
2947
|
}
|
|
2636
2948
|
} else {
|
|
2637
|
-
|
|
2949
|
+
fs15.writeFileSync(p, `${header}${entry}`);
|
|
2638
2950
|
}
|
|
2639
2951
|
}
|
|
2640
2952
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -2685,13 +2997,13 @@ var releaseFlow = async (ctx) => {
|
|
|
2685
2997
|
};
|
|
2686
2998
|
async function runPrepare(args) {
|
|
2687
2999
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
2688
|
-
const pkgPath =
|
|
2689
|
-
if (!
|
|
3000
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
3001
|
+
if (!fs15.existsSync(pkgPath)) {
|
|
2690
3002
|
ctx.output.exitCode = 99;
|
|
2691
3003
|
ctx.output.reason = "release prepare: package.json not found";
|
|
2692
3004
|
return;
|
|
2693
3005
|
}
|
|
2694
|
-
const pkg = JSON.parse(
|
|
3006
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2695
3007
|
if (typeof pkg.version !== "string") {
|
|
2696
3008
|
ctx.output.exitCode = 99;
|
|
2697
3009
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -2745,10 +3057,10 @@ ${entry}
|
|
|
2745
3057
|
Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
2746
3058
|
let prUrl = "";
|
|
2747
3059
|
try {
|
|
2748
|
-
prUrl = gh(
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
).trim();
|
|
3060
|
+
prUrl = gh(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
|
|
3061
|
+
input: body,
|
|
3062
|
+
cwd
|
|
3063
|
+
}).trim();
|
|
2752
3064
|
} catch (err) {
|
|
2753
3065
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2754
3066
|
ctx.output.exitCode = 4;
|
|
@@ -2762,8 +3074,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
2762
3074
|
}
|
|
2763
3075
|
async function runFinalize(args) {
|
|
2764
3076
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
2765
|
-
const pkgPath =
|
|
2766
|
-
const pkg = JSON.parse(
|
|
3077
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
3078
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2767
3079
|
if (typeof pkg.version !== "string") {
|
|
2768
3080
|
ctx.output.exitCode = 99;
|
|
2769
3081
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -2820,20 +3132,14 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
2820
3132
|
}
|
|
2821
3133
|
let releaseUrl = "";
|
|
2822
3134
|
try {
|
|
2823
|
-
const releaseArgs = [
|
|
2824
|
-
"release",
|
|
2825
|
-
"create",
|
|
2826
|
-
tag,
|
|
2827
|
-
"--title",
|
|
2828
|
-
tag,
|
|
2829
|
-
"--notes",
|
|
2830
|
-
`Release ${tag} \u2014 automated by kody2.`
|
|
2831
|
-
];
|
|
3135
|
+
const releaseArgs = ["release", "create", tag, "--title", tag, "--notes", `Release ${tag} \u2014 automated by kody2.`];
|
|
2832
3136
|
if (releaseCfg.draftRelease) releaseArgs.push("--draft");
|
|
2833
3137
|
releaseUrl = gh(releaseArgs, { cwd }).trim();
|
|
2834
3138
|
} catch (err) {
|
|
2835
|
-
process.stderr.write(
|
|
2836
|
-
`)
|
|
3139
|
+
process.stderr.write(
|
|
3140
|
+
`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
|
|
3141
|
+
`
|
|
3142
|
+
);
|
|
2837
3143
|
}
|
|
2838
3144
|
if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
|
|
2839
3145
|
const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
|
|
@@ -2852,6 +3158,35 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
2852
3158
|
`);
|
|
2853
3159
|
}
|
|
2854
3160
|
|
|
3161
|
+
// src/scripts/requireFeedbackActions.ts
|
|
3162
|
+
var MIN_ITEMS = 1;
|
|
3163
|
+
var requireFeedbackActions = async (ctx, profile) => {
|
|
3164
|
+
if (!ctx.data.agentDone) return;
|
|
3165
|
+
const actions = String(ctx.data.feedbackActions ?? "").trim();
|
|
3166
|
+
const items = countActionItems(actions);
|
|
3167
|
+
if (items >= MIN_ITEMS) return;
|
|
3168
|
+
const reason = actions.length === 0 ? "agent omitted required FEEDBACK_ACTIONS block \u2014 cannot verify that review feedback was addressed" : "agent FEEDBACK_ACTIONS block listed no items \u2014 cannot verify that review feedback was addressed";
|
|
3169
|
+
ctx.data.agentDone = false;
|
|
3170
|
+
ctx.data.agentFailureReason = reason;
|
|
3171
|
+
const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
|
|
3172
|
+
const failedAction = {
|
|
3173
|
+
type: `${modeSeg}_FAILED`,
|
|
3174
|
+
payload: { reason },
|
|
3175
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3176
|
+
};
|
|
3177
|
+
ctx.data.action = failedAction;
|
|
3178
|
+
};
|
|
3179
|
+
function countActionItems(block) {
|
|
3180
|
+
if (!block.trim()) return 0;
|
|
3181
|
+
const lines = block.split("\n");
|
|
3182
|
+
let count = 0;
|
|
3183
|
+
for (const raw of lines) {
|
|
3184
|
+
const line = raw.trim();
|
|
3185
|
+
if (/^[-*]\s+/.test(line)) count++;
|
|
3186
|
+
}
|
|
3187
|
+
return count;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
2855
3190
|
// src/scripts/resolveArtifacts.ts
|
|
2856
3191
|
var resolveArtifacts = async (ctx, profile) => {
|
|
2857
3192
|
if (profile.inputArtifacts.length === 0) return;
|
|
@@ -3101,11 +3436,7 @@ var syncFlow = async (ctx) => {
|
|
|
3101
3436
|
ctx.output.reason = `merged origin/${baseBranch} into ${ctx.data.branch}`;
|
|
3102
3437
|
const runUrl = getRunUrl();
|
|
3103
3438
|
const runSuffix = runUrl ? ` ([logs](${runUrl}))` : "";
|
|
3104
|
-
tryPostPr5(
|
|
3105
|
-
prNumber,
|
|
3106
|
-
`\u2705 kody2 sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`,
|
|
3107
|
-
ctx.cwd
|
|
3108
|
-
);
|
|
3439
|
+
tryPostPr5(prNumber, `\u2705 kody2 sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`, ctx.cwd);
|
|
3109
3440
|
};
|
|
3110
3441
|
function bail2(ctx, prNumber, reason) {
|
|
3111
3442
|
ctx.output.exitCode = 1;
|
|
@@ -3145,7 +3476,7 @@ import { spawn as spawn2 } from "child_process";
|
|
|
3145
3476
|
var TAIL_CHARS = 4e3;
|
|
3146
3477
|
var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3147
3478
|
function runCommand(command, cwd) {
|
|
3148
|
-
return new Promise((
|
|
3479
|
+
return new Promise((resolve4) => {
|
|
3149
3480
|
const start = Date.now();
|
|
3150
3481
|
const child = spawn2(command, {
|
|
3151
3482
|
cwd,
|
|
@@ -3174,11 +3505,11 @@ function runCommand(command, cwd) {
|
|
|
3174
3505
|
child.on("exit", (code) => {
|
|
3175
3506
|
clearTimeout(timer);
|
|
3176
3507
|
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
3177
|
-
|
|
3508
|
+
resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
3178
3509
|
});
|
|
3179
3510
|
child.on("error", (err) => {
|
|
3180
3511
|
clearTimeout(timer);
|
|
3181
|
-
|
|
3512
|
+
resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
3182
3513
|
});
|
|
3183
3514
|
});
|
|
3184
3515
|
}
|
|
@@ -3237,19 +3568,7 @@ function readWatchConfig(ctx) {
|
|
|
3237
3568
|
function findStalePrs(cwd, staleDays, now = /* @__PURE__ */ new Date()) {
|
|
3238
3569
|
let raw = "";
|
|
3239
3570
|
try {
|
|
3240
|
-
raw = gh(
|
|
3241
|
-
[
|
|
3242
|
-
"pr",
|
|
3243
|
-
"list",
|
|
3244
|
-
"--state",
|
|
3245
|
-
"open",
|
|
3246
|
-
"--limit",
|
|
3247
|
-
"100",
|
|
3248
|
-
"--json",
|
|
3249
|
-
"number,title,url,updatedAt"
|
|
3250
|
-
],
|
|
3251
|
-
{ cwd }
|
|
3252
|
-
);
|
|
3571
|
+
raw = gh(["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,url,updatedAt"], { cwd });
|
|
3253
3572
|
} catch {
|
|
3254
3573
|
return [];
|
|
3255
3574
|
}
|
|
@@ -3295,8 +3614,10 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3295
3614
|
try {
|
|
3296
3615
|
postIssueComment(reportIssueNumber, report, ctx.cwd);
|
|
3297
3616
|
} catch (err) {
|
|
3298
|
-
process.stderr.write(
|
|
3299
|
-
`)
|
|
3617
|
+
process.stderr.write(
|
|
3618
|
+
`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
3619
|
+
`
|
|
3620
|
+
);
|
|
3300
3621
|
}
|
|
3301
3622
|
}
|
|
3302
3623
|
ctx.output.exitCode = 0;
|
|
@@ -3304,7 +3625,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3304
3625
|
};
|
|
3305
3626
|
|
|
3306
3627
|
// src/scripts/writeRunSummary.ts
|
|
3307
|
-
import * as
|
|
3628
|
+
import * as fs16 from "fs";
|
|
3308
3629
|
var writeRunSummary = async (ctx, profile) => {
|
|
3309
3630
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3310
3631
|
if (!summaryPath) return;
|
|
@@ -3326,7 +3647,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
3326
3647
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
3327
3648
|
lines.push("");
|
|
3328
3649
|
try {
|
|
3329
|
-
|
|
3650
|
+
fs16.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
3330
3651
|
`);
|
|
3331
3652
|
} catch {
|
|
3332
3653
|
}
|
|
@@ -3353,6 +3674,7 @@ var preflightScripts = {
|
|
|
3353
3674
|
};
|
|
3354
3675
|
var postflightScripts = {
|
|
3355
3676
|
parseAgentResult: parseAgentResult2,
|
|
3677
|
+
requireFeedbackActions,
|
|
3356
3678
|
verify,
|
|
3357
3679
|
checkCoverageWithRetry,
|
|
3358
3680
|
commitAndPush: commitAndPush2,
|
|
@@ -3471,9 +3793,9 @@ async function runExecutable(profileName, input) {
|
|
|
3471
3793
|
data: {},
|
|
3472
3794
|
output: { exitCode: 0 }
|
|
3473
3795
|
};
|
|
3474
|
-
const ndjsonDir =
|
|
3796
|
+
const ndjsonDir = path14.join(input.cwd, ".kody2");
|
|
3475
3797
|
const invokeAgent = async (prompt) => {
|
|
3476
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
3798
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
3477
3799
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
3478
3800
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
3479
3801
|
return runAgent({
|
|
@@ -3489,6 +3811,7 @@ async function runExecutable(profileName, input) {
|
|
|
3489
3811
|
mcpServers: profile.claudeCode.mcpServers,
|
|
3490
3812
|
pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
|
|
3491
3813
|
maxTurns: profile.claudeCode.maxTurns,
|
|
3814
|
+
maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
|
|
3492
3815
|
systemPromptAppend: profile.claudeCode.systemPromptAppend,
|
|
3493
3816
|
settingSources: profile.claudeCode.settingSources
|
|
3494
3817
|
});
|
|
@@ -3539,17 +3862,17 @@ async function runExecutable(profileName, input) {
|
|
|
3539
3862
|
}
|
|
3540
3863
|
}
|
|
3541
3864
|
function resolveProfilePath(profileName) {
|
|
3542
|
-
const here =
|
|
3865
|
+
const here = path14.dirname(new URL(import.meta.url).pathname);
|
|
3543
3866
|
const candidates = [
|
|
3544
|
-
|
|
3867
|
+
path14.join(here, "executables", profileName, "profile.json"),
|
|
3545
3868
|
// same-dir sibling (dev)
|
|
3546
|
-
|
|
3869
|
+
path14.join(here, "..", "executables", profileName, "profile.json"),
|
|
3547
3870
|
// up one (prod: dist/bin → dist/executables)
|
|
3548
|
-
|
|
3871
|
+
path14.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
3549
3872
|
// fallback
|
|
3550
3873
|
];
|
|
3551
3874
|
for (const c of candidates) {
|
|
3552
|
-
if (
|
|
3875
|
+
if (fs17.existsSync(c)) return c;
|
|
3553
3876
|
}
|
|
3554
3877
|
return candidates[0];
|
|
3555
3878
|
}
|
|
@@ -3640,95 +3963,6 @@ function finish(out) {
|
|
|
3640
3963
|
return out;
|
|
3641
3964
|
}
|
|
3642
3965
|
|
|
3643
|
-
// src/kody2-cli.ts
|
|
3644
|
-
import { execFileSync as execFileSync15 } from "child_process";
|
|
3645
|
-
import * as fs16 from "fs";
|
|
3646
|
-
import * as path13 from "path";
|
|
3647
|
-
|
|
3648
|
-
// src/dispatch.ts
|
|
3649
|
-
import * as fs15 from "fs";
|
|
3650
|
-
function autoDispatch(opts) {
|
|
3651
|
-
const explicit = opts?.explicit;
|
|
3652
|
-
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
3653
|
-
return {
|
|
3654
|
-
executable: "run",
|
|
3655
|
-
cliArgs: { issue: explicit.issueNumber },
|
|
3656
|
-
target: explicit.issueNumber
|
|
3657
|
-
};
|
|
3658
|
-
}
|
|
3659
|
-
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
3660
|
-
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3661
|
-
if (!eventName || !eventPath || !fs15.existsSync(eventPath)) return null;
|
|
3662
|
-
let event = {};
|
|
3663
|
-
try {
|
|
3664
|
-
event = JSON.parse(fs15.readFileSync(eventPath, "utf-8"));
|
|
3665
|
-
} catch {
|
|
3666
|
-
return null;
|
|
3667
|
-
}
|
|
3668
|
-
if (eventName === "workflow_dispatch") {
|
|
3669
|
-
const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
|
|
3670
|
-
if (!Number.isNaN(n) && n > 0) {
|
|
3671
|
-
return { executable: "run", cliArgs: { issue: n }, target: n };
|
|
3672
|
-
}
|
|
3673
|
-
return null;
|
|
3674
|
-
}
|
|
3675
|
-
if (eventName !== "issue_comment") return null;
|
|
3676
|
-
const body = String(event.comment?.body ?? "").toLowerCase();
|
|
3677
|
-
const targetNum = Number(event.issue?.number ?? 0);
|
|
3678
|
-
const isPr = !!event.issue?.pull_request;
|
|
3679
|
-
if (!targetNum) return null;
|
|
3680
|
-
const afterTag = extractAfterTag(body);
|
|
3681
|
-
if (isPr) {
|
|
3682
|
-
if (/\bfix-ci\b/.test(afterTag)) {
|
|
3683
|
-
return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
|
|
3684
|
-
}
|
|
3685
|
-
if (/\bresolve\b/.test(afterTag)) {
|
|
3686
|
-
return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
|
|
3687
|
-
}
|
|
3688
|
-
if (/\breview\b/.test(afterTag)) {
|
|
3689
|
-
return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
|
|
3690
|
-
}
|
|
3691
|
-
if (/\bsync\b/.test(afterTag)) {
|
|
3692
|
-
return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
|
|
3693
|
-
}
|
|
3694
|
-
const feedback = extractFeedback(afterTag);
|
|
3695
|
-
return {
|
|
3696
|
-
executable: "fix",
|
|
3697
|
-
cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
|
|
3698
|
-
target: targetNum
|
|
3699
|
-
};
|
|
3700
|
-
}
|
|
3701
|
-
const sub = extractSubcommand(afterTag);
|
|
3702
|
-
const defaultExec = opts?.config?.defaultExecutable ?? "run";
|
|
3703
|
-
if (!sub) {
|
|
3704
|
-
return asDispatch(defaultExec, targetNum);
|
|
3705
|
-
}
|
|
3706
|
-
if (sub === "orchestrate" || sub === "orchestrator") {
|
|
3707
|
-
return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
|
|
3708
|
-
}
|
|
3709
|
-
if (sub === "build") {
|
|
3710
|
-
return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
|
|
3711
|
-
}
|
|
3712
|
-
return asDispatch(sub, targetNum);
|
|
3713
|
-
}
|
|
3714
|
-
function asDispatch(executable, target) {
|
|
3715
|
-
return { executable, cliArgs: { issue: target }, target };
|
|
3716
|
-
}
|
|
3717
|
-
function extractAfterTag(body) {
|
|
3718
|
-
const idx = body.indexOf("@kody2");
|
|
3719
|
-
if (idx === -1) return body;
|
|
3720
|
-
return body.slice(idx + "@kody2".length).trim();
|
|
3721
|
-
}
|
|
3722
|
-
function extractSubcommand(afterTag) {
|
|
3723
|
-
const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
|
|
3724
|
-
if (!match) return null;
|
|
3725
|
-
return match[1];
|
|
3726
|
-
}
|
|
3727
|
-
function extractFeedback(afterTag) {
|
|
3728
|
-
const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
|
|
3729
|
-
return cleaned.length > 0 ? cleaned : void 0;
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
3966
|
// src/kody2-cli.ts
|
|
3733
3967
|
var CI_HELP = `kody2 ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
|
|
3734
3968
|
|
|
@@ -3814,9 +4048,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
3814
4048
|
return token;
|
|
3815
4049
|
}
|
|
3816
4050
|
function detectPackageManager2(cwd) {
|
|
3817
|
-
if (
|
|
3818
|
-
if (
|
|
3819
|
-
if (
|
|
4051
|
+
if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4052
|
+
if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
|
|
4053
|
+
if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
|
|
3820
4054
|
return "npm";
|
|
3821
4055
|
}
|
|
3822
4056
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -3896,11 +4130,11 @@ function configureGitIdentity(cwd) {
|
|
|
3896
4130
|
}
|
|
3897
4131
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
3898
4132
|
if (!issueNumber) return;
|
|
3899
|
-
const logPath =
|
|
4133
|
+
const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
|
|
3900
4134
|
let tail = "";
|
|
3901
4135
|
try {
|
|
3902
|
-
if (
|
|
3903
|
-
const content =
|
|
4136
|
+
if (fs18.existsSync(logPath)) {
|
|
4137
|
+
const content = fs18.readFileSync(logPath, "utf-8");
|
|
3904
4138
|
tail = content.slice(-3e3);
|
|
3905
4139
|
}
|
|
3906
4140
|
} catch {
|
|
@@ -3925,7 +4159,7 @@ async function runCi(argv) {
|
|
|
3925
4159
|
return 0;
|
|
3926
4160
|
}
|
|
3927
4161
|
const args = parseCiArgs(argv);
|
|
3928
|
-
const cwd = args.cwd ?
|
|
4162
|
+
const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
|
|
3929
4163
|
let earlyConfig;
|
|
3930
4164
|
try {
|
|
3931
4165
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4013,6 +4247,148 @@ ${CI_HELP}`);
|
|
|
4013
4247
|
}
|
|
4014
4248
|
}
|
|
4015
4249
|
|
|
4250
|
+
// src/chat-cli.ts
|
|
4251
|
+
var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
|
|
4252
|
+
var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
|
|
4253
|
+
|
|
4254
|
+
Usage:
|
|
4255
|
+
kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
|
|
4256
|
+
[--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
|
|
4257
|
+
|
|
4258
|
+
All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
|
|
4259
|
+
CLI flags take precedence over env. SESSION_ID is required.
|
|
4260
|
+
|
|
4261
|
+
Exit codes:
|
|
4262
|
+
0 reply emitted successfully
|
|
4263
|
+
64 bad inputs (missing session, empty history)
|
|
4264
|
+
99 runtime failure (agent crash, LiteLLM failure)
|
|
4265
|
+
`;
|
|
4266
|
+
function parseChatArgs(argv, env = process.env) {
|
|
4267
|
+
const result = { errors: [] };
|
|
4268
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4269
|
+
const arg = argv[i];
|
|
4270
|
+
if (arg === "--session") result.sessionId = argv[++i];
|
|
4271
|
+
else if (arg === "--message") result.initMessage = argv[++i];
|
|
4272
|
+
else if (arg === "--model") result.model = argv[++i];
|
|
4273
|
+
else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
|
|
4274
|
+
else if (arg === "--cwd") result.cwd = argv[++i];
|
|
4275
|
+
else if (arg === "--verbose") result.verbose = true;
|
|
4276
|
+
else if (arg === "--quiet") result.quiet = true;
|
|
4277
|
+
else if (arg === "--help" || arg === "-h") result.errors.push("__HELP__");
|
|
4278
|
+
else if (arg?.startsWith("--")) result.errors.push(`unknown arg: ${arg}`);
|
|
4279
|
+
else if (arg) result.errors.push(`unexpected positional: ${arg}`);
|
|
4280
|
+
}
|
|
4281
|
+
result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
|
|
4282
|
+
result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
|
|
4283
|
+
result.model = result.model ?? env.MODEL ?? void 0;
|
|
4284
|
+
result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
|
|
4285
|
+
for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
|
|
4286
|
+
const v = result[key];
|
|
4287
|
+
if (typeof v === "string" && v.trim() === "") result[key] = void 0;
|
|
4288
|
+
}
|
|
4289
|
+
if (!result.sessionId && !result.errors.includes("__HELP__")) {
|
|
4290
|
+
result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
4291
|
+
}
|
|
4292
|
+
return result;
|
|
4293
|
+
}
|
|
4294
|
+
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4295
|
+
const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
4296
|
+
const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
4297
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
|
|
4298
|
+
if (paths.length === 0) return;
|
|
4299
|
+
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4300
|
+
try {
|
|
4301
|
+
execFileSync16("git", ["add", ...paths], opts);
|
|
4302
|
+
execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
|
|
4303
|
+
execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
|
|
4304
|
+
} catch (err) {
|
|
4305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4306
|
+
process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
|
|
4307
|
+
`);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function tryLoadConfig(cwd) {
|
|
4311
|
+
try {
|
|
4312
|
+
return loadConfig(cwd);
|
|
4313
|
+
} catch {
|
|
4314
|
+
return null;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
function buildSink(cwd, sessionId, dashboardUrl) {
|
|
4318
|
+
const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
|
|
4319
|
+
if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
|
|
4320
|
+
return new TeeSink(sinks);
|
|
4321
|
+
}
|
|
4322
|
+
async function runChat(argv) {
|
|
4323
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
4324
|
+
process.stdout.write(CHAT_HELP);
|
|
4325
|
+
return 0;
|
|
4326
|
+
}
|
|
4327
|
+
const args = parseChatArgs(argv);
|
|
4328
|
+
if (args.errors.length > 0 && !args.errors.includes("__HELP__")) {
|
|
4329
|
+
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
4330
|
+
`);
|
|
4331
|
+
process.stderr.write(`
|
|
4332
|
+
${CHAT_HELP}`);
|
|
4333
|
+
return 64;
|
|
4334
|
+
}
|
|
4335
|
+
const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
|
|
4336
|
+
const sessionId = args.sessionId;
|
|
4337
|
+
const unpackedSecrets = unpackAllSecrets();
|
|
4338
|
+
if (unpackedSecrets > 0) {
|
|
4339
|
+
process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
|
|
4340
|
+
`);
|
|
4341
|
+
}
|
|
4342
|
+
resolveAuthToken();
|
|
4343
|
+
configureGitIdentity(cwd);
|
|
4344
|
+
const config = tryLoadConfig(cwd);
|
|
4345
|
+
const modelSpec = args.model ?? config?.agent.model ?? DEFAULT_MODEL;
|
|
4346
|
+
let model;
|
|
4347
|
+
try {
|
|
4348
|
+
model = parseProviderModel(modelSpec);
|
|
4349
|
+
} catch (err) {
|
|
4350
|
+
process.stderr.write(`error: invalid model '${modelSpec}': ${err instanceof Error ? err.message : String(err)}
|
|
4351
|
+
`);
|
|
4352
|
+
return 64;
|
|
4353
|
+
}
|
|
4354
|
+
let litellm = null;
|
|
4355
|
+
try {
|
|
4356
|
+
litellm = await startLitellmIfNeeded(model, cwd);
|
|
4357
|
+
} catch (err) {
|
|
4358
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4359
|
+
const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4360
|
+
await sink2.emit({
|
|
4361
|
+
event: "chat.error",
|
|
4362
|
+
payload: { sessionId, error: `litellm startup failed: ${msg}` },
|
|
4363
|
+
runId: makeRunId(sessionId, "error"),
|
|
4364
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4365
|
+
});
|
|
4366
|
+
return 99;
|
|
4367
|
+
}
|
|
4368
|
+
const sessionFile = sessionFilePath(cwd, sessionId);
|
|
4369
|
+
if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
|
|
4370
|
+
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4371
|
+
try {
|
|
4372
|
+
const result = await runChatTurn({
|
|
4373
|
+
sessionId,
|
|
4374
|
+
sessionFile,
|
|
4375
|
+
cwd,
|
|
4376
|
+
model,
|
|
4377
|
+
litellmUrl: litellm?.url ?? null,
|
|
4378
|
+
sink,
|
|
4379
|
+
verbose: args.verbose,
|
|
4380
|
+
quiet: args.quiet
|
|
4381
|
+
});
|
|
4382
|
+
commitChatFiles(cwd, sessionId, args.verbose ?? false);
|
|
4383
|
+
return result.exitCode;
|
|
4384
|
+
} finally {
|
|
4385
|
+
try {
|
|
4386
|
+
litellm?.kill();
|
|
4387
|
+
} catch {
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4016
4392
|
// src/entry.ts
|
|
4017
4393
|
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
4018
4394
|
|
|
@@ -4024,6 +4400,7 @@ Usage:
|
|
|
4024
4400
|
kody2 review --pr <N> [--cwd <path>] [--verbose|--quiet]
|
|
4025
4401
|
kody2 <other> [--cwd <path>] [--verbose|--quiet]
|
|
4026
4402
|
kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
|
|
4403
|
+
kody2 chat [chat flags \u2014 see: kody2 chat --help]
|
|
4027
4404
|
kody2 help
|
|
4028
4405
|
kody2 version
|
|
4029
4406
|
|
|
@@ -4050,6 +4427,9 @@ function parseArgs(argv) {
|
|
|
4050
4427
|
if (cmd === "ci") {
|
|
4051
4428
|
return { ...result, command: "ci", ciArgv: argv.slice(1) };
|
|
4052
4429
|
}
|
|
4430
|
+
if (cmd === "chat") {
|
|
4431
|
+
return { ...result, command: "chat", chatArgv: argv.slice(1) };
|
|
4432
|
+
}
|
|
4053
4433
|
if (hasExecutable(cmd)) {
|
|
4054
4434
|
result.command = "__executable__";
|
|
4055
4435
|
result.executableName = cmd;
|
|
@@ -4090,6 +4470,18 @@ ${HELP_TEXT}`);
|
|
|
4090
4470
|
process.stderr.write(`[kody2] fatal: ${msg}
|
|
4091
4471
|
`);
|
|
4092
4472
|
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
4473
|
+
`);
|
|
4474
|
+
return 99;
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
if (args.command === "chat") {
|
|
4478
|
+
try {
|
|
4479
|
+
return await runChat(args.chatArgv ?? []);
|
|
4480
|
+
} catch (err) {
|
|
4481
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4482
|
+
process.stderr.write(`[kody2] fatal: ${msg}
|
|
4483
|
+
`);
|
|
4484
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
4093
4485
|
`);
|
|
4094
4486
|
return 99;
|
|
4095
4487
|
}
|