@kody-ade/kody-engine 0.2.22 → 0.2.27
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 +689 -302
- package/dist/executables/fix/profile.json +3 -0
- package/dist/executables/fix/prompt.md +14 -7
- package/package.json +1 -1
- 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.27",
|
|
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",
|
|
@@ -336,11 +400,228 @@ async function runAgent(opts) {
|
|
|
336
400
|
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
337
401
|
}
|
|
338
402
|
|
|
403
|
+
// src/chat/session.ts
|
|
404
|
+
import * as fs4 from "fs";
|
|
405
|
+
import * as path4 from "path";
|
|
406
|
+
function sessionFilePath(cwd, sessionId) {
|
|
407
|
+
return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
408
|
+
}
|
|
409
|
+
function readSession(file) {
|
|
410
|
+
if (!fs4.existsSync(file)) return [];
|
|
411
|
+
const raw = fs4.readFileSync(file, "utf-8").trim();
|
|
412
|
+
if (!raw) return [];
|
|
413
|
+
const turns = [];
|
|
414
|
+
for (const line of raw.split("\n")) {
|
|
415
|
+
if (!line.trim()) continue;
|
|
416
|
+
try {
|
|
417
|
+
const parsed = JSON.parse(line);
|
|
418
|
+
if (parsed.role !== "user" && parsed.role !== "assistant") continue;
|
|
419
|
+
if (typeof parsed.content !== "string") continue;
|
|
420
|
+
turns.push(parsed);
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return turns;
|
|
425
|
+
}
|
|
426
|
+
function appendTurn(file, turn) {
|
|
427
|
+
fs4.mkdirSync(path4.dirname(file), { recursive: true });
|
|
428
|
+
const line = JSON.stringify({
|
|
429
|
+
role: turn.role,
|
|
430
|
+
content: turn.content,
|
|
431
|
+
timestamp: turn.timestamp,
|
|
432
|
+
toolCalls: turn.toolCalls ?? []
|
|
433
|
+
});
|
|
434
|
+
fs4.appendFileSync(file, `${line}
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
function seedInitialMessage(file, message) {
|
|
438
|
+
if (!message.trim()) return false;
|
|
439
|
+
const turns = readSession(file);
|
|
440
|
+
const lastUser = [...turns].reverse().find((t) => t.role === "user");
|
|
441
|
+
if (lastUser && lastUser.content === message) return false;
|
|
442
|
+
appendTurn(file, {
|
|
443
|
+
role: "user",
|
|
444
|
+
content: message,
|
|
445
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
446
|
+
});
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/chat/loop.ts
|
|
451
|
+
var CHAT_SYSTEM_PROMPT = [
|
|
452
|
+
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
|
|
453
|
+
"latest message using the full conversation below as context. Keep replies focused,",
|
|
454
|
+
"technical when appropriate, and formatted in Markdown. Use the available tools to",
|
|
455
|
+
"read repository code or execute small checks when it helps you answer \u2014 otherwise",
|
|
456
|
+
"reply directly. Do not invent file paths, commit SHAs, or command output."
|
|
457
|
+
].join("\n");
|
|
458
|
+
async function runChatTurn(opts) {
|
|
459
|
+
const turns = readSession(opts.sessionFile);
|
|
460
|
+
if (turns.length === 0) {
|
|
461
|
+
const error = "session file is empty \u2014 nothing to reply to";
|
|
462
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
463
|
+
return { exitCode: 64, error };
|
|
464
|
+
}
|
|
465
|
+
const lastTurn = turns[turns.length - 1];
|
|
466
|
+
if (lastTurn.role !== "user") {
|
|
467
|
+
const error = "last turn is not a user message \u2014 assistant already replied";
|
|
468
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
469
|
+
return { exitCode: 64, error };
|
|
470
|
+
}
|
|
471
|
+
const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
|
|
472
|
+
const invoke = opts.invokeAgent ?? ((p) => runAgent({
|
|
473
|
+
prompt: p,
|
|
474
|
+
model: opts.model,
|
|
475
|
+
cwd: opts.cwd,
|
|
476
|
+
litellmUrl: opts.litellmUrl,
|
|
477
|
+
verbose: opts.verbose,
|
|
478
|
+
quiet: opts.quiet
|
|
479
|
+
}));
|
|
480
|
+
let result;
|
|
481
|
+
try {
|
|
482
|
+
result = await invoke(prompt);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
485
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
486
|
+
return { exitCode: 99, error };
|
|
487
|
+
}
|
|
488
|
+
if (result.outcome !== "completed") {
|
|
489
|
+
const error = result.error ?? "agent did not complete";
|
|
490
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
491
|
+
return { exitCode: 99, error };
|
|
492
|
+
}
|
|
493
|
+
const reply = result.finalText.trim();
|
|
494
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
495
|
+
appendTurn(opts.sessionFile, {
|
|
496
|
+
role: "assistant",
|
|
497
|
+
content: reply,
|
|
498
|
+
timestamp: now
|
|
499
|
+
});
|
|
500
|
+
await emit(opts.sink, "chat.message", opts.sessionId, "message", {
|
|
501
|
+
sessionId: opts.sessionId,
|
|
502
|
+
role: "assistant",
|
|
503
|
+
content: reply,
|
|
504
|
+
timestamp: now
|
|
505
|
+
});
|
|
506
|
+
await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
|
|
507
|
+
return { exitCode: 0, reply };
|
|
508
|
+
}
|
|
509
|
+
function buildPrompt(turns, systemPrompt) {
|
|
510
|
+
const header = `System: ${systemPrompt}`;
|
|
511
|
+
const body = turns.map((t) => `${t.role === "user" ? "User" : "Assistant"}: ${t.content}`).join("\n\n");
|
|
512
|
+
return `${header}
|
|
513
|
+
|
|
514
|
+
${body}
|
|
515
|
+
|
|
516
|
+
Assistant:`;
|
|
517
|
+
}
|
|
518
|
+
async function emit(sink, type, sessionId, suffix, payload) {
|
|
519
|
+
await sink.emit({
|
|
520
|
+
event: type,
|
|
521
|
+
payload,
|
|
522
|
+
runId: makeRunId(sessionId, suffix),
|
|
523
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/kody2-cli.ts
|
|
528
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
529
|
+
import * as fs18 from "fs";
|
|
530
|
+
import * as path15 from "path";
|
|
531
|
+
|
|
532
|
+
// src/dispatch.ts
|
|
533
|
+
import * as fs5 from "fs";
|
|
534
|
+
function autoDispatch(opts) {
|
|
535
|
+
const explicit = opts?.explicit;
|
|
536
|
+
if (explicit?.issueNumber && explicit.issueNumber > 0) {
|
|
537
|
+
return {
|
|
538
|
+
executable: "run",
|
|
539
|
+
cliArgs: { issue: explicit.issueNumber },
|
|
540
|
+
target: explicit.issueNumber
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
544
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
545
|
+
if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
|
|
546
|
+
let event = {};
|
|
547
|
+
try {
|
|
548
|
+
event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
|
|
549
|
+
} catch {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
if (eventName === "workflow_dispatch") {
|
|
553
|
+
const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
|
|
554
|
+
if (!Number.isNaN(n) && n > 0) {
|
|
555
|
+
return { executable: "run", cliArgs: { issue: n }, target: n };
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
if (eventName !== "issue_comment") return null;
|
|
560
|
+
const body = String(event.comment?.body ?? "").toLowerCase();
|
|
561
|
+
const targetNum = Number(event.issue?.number ?? 0);
|
|
562
|
+
const isPr = !!event.issue?.pull_request;
|
|
563
|
+
if (!targetNum) return null;
|
|
564
|
+
const afterTag = extractAfterTag(body);
|
|
565
|
+
if (isPr) {
|
|
566
|
+
if (/\bfix-ci\b/.test(afterTag)) {
|
|
567
|
+
return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
|
|
568
|
+
}
|
|
569
|
+
if (/\bresolve\b/.test(afterTag)) {
|
|
570
|
+
return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
|
|
571
|
+
}
|
|
572
|
+
if (/\breview\b/.test(afterTag)) {
|
|
573
|
+
return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
|
|
574
|
+
}
|
|
575
|
+
if (/\bsync\b/.test(afterTag)) {
|
|
576
|
+
return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
|
|
577
|
+
}
|
|
578
|
+
const feedback = extractFeedback(afterTag);
|
|
579
|
+
return {
|
|
580
|
+
executable: "fix",
|
|
581
|
+
cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
|
|
582
|
+
target: targetNum
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const sub = extractSubcommand(afterTag);
|
|
586
|
+
const defaultExec = opts?.config?.defaultExecutable ?? "run";
|
|
587
|
+
if (!sub) {
|
|
588
|
+
return asDispatch(defaultExec, targetNum);
|
|
589
|
+
}
|
|
590
|
+
if (sub === "orchestrate" || sub === "orchestrator") {
|
|
591
|
+
return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
|
|
592
|
+
}
|
|
593
|
+
if (sub === "build") {
|
|
594
|
+
return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
|
|
595
|
+
}
|
|
596
|
+
return asDispatch(sub, targetNum);
|
|
597
|
+
}
|
|
598
|
+
function asDispatch(executable, target) {
|
|
599
|
+
return { executable, cliArgs: { issue: target }, target };
|
|
600
|
+
}
|
|
601
|
+
function extractAfterTag(body) {
|
|
602
|
+
const idx = body.indexOf("@kody2");
|
|
603
|
+
if (idx === -1) return body;
|
|
604
|
+
return body.slice(idx + "@kody2".length).trim();
|
|
605
|
+
}
|
|
606
|
+
function extractSubcommand(afterTag) {
|
|
607
|
+
const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
|
|
608
|
+
if (!match) return null;
|
|
609
|
+
return match[1];
|
|
610
|
+
}
|
|
611
|
+
function extractFeedback(afterTag) {
|
|
612
|
+
const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
|
|
613
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/executor.ts
|
|
617
|
+
import * as fs17 from "fs";
|
|
618
|
+
import * as path14 from "path";
|
|
619
|
+
|
|
339
620
|
// src/litellm.ts
|
|
340
621
|
import { execFileSync, spawn } from "child_process";
|
|
341
|
-
import * as
|
|
622
|
+
import * as fs6 from "fs";
|
|
342
623
|
import * as os from "os";
|
|
343
|
-
import * as
|
|
624
|
+
import * as path5 from "path";
|
|
344
625
|
async function checkLitellmHealth(url) {
|
|
345
626
|
try {
|
|
346
627
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -380,20 +661,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
380
661
|
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
381
662
|
}
|
|
382
663
|
}
|
|
383
|
-
const configPath =
|
|
384
|
-
|
|
664
|
+
const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
665
|
+
fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
385
666
|
const portMatch = url.match(/:(\d+)/);
|
|
386
667
|
const port = portMatch ? portMatch[1] : "4000";
|
|
387
668
|
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
388
669
|
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
389
|
-
const logPath =
|
|
390
|
-
const outFd =
|
|
670
|
+
const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
671
|
+
const outFd = fs6.openSync(logPath, "w");
|
|
391
672
|
const child = spawn(cmd, args, {
|
|
392
673
|
stdio: ["ignore", outFd, outFd],
|
|
393
674
|
detached: true,
|
|
394
675
|
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
395
676
|
});
|
|
396
|
-
|
|
677
|
+
fs6.closeSync(outFd);
|
|
397
678
|
for (let i = 0; i < 30; i++) {
|
|
398
679
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
399
680
|
if (await checkLitellmHealth(url)) {
|
|
@@ -410,7 +691,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
410
691
|
}
|
|
411
692
|
let logTail = "";
|
|
412
693
|
try {
|
|
413
|
-
logTail =
|
|
694
|
+
logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
414
695
|
} catch {
|
|
415
696
|
}
|
|
416
697
|
try {
|
|
@@ -421,10 +702,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
421
702
|
${logTail}`);
|
|
422
703
|
}
|
|
423
704
|
function readDotenvApiKeys(projectDir) {
|
|
424
|
-
const dotenvPath =
|
|
425
|
-
if (!
|
|
705
|
+
const dotenvPath = path5.join(projectDir, ".env");
|
|
706
|
+
if (!fs6.existsSync(dotenvPath)) return {};
|
|
426
707
|
const result = {};
|
|
427
|
-
for (const rawLine of
|
|
708
|
+
for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
428
709
|
const line = rawLine.trim();
|
|
429
710
|
if (!line || line.startsWith("#")) continue;
|
|
430
711
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
@@ -447,8 +728,8 @@ function stripBlockingEnv(env) {
|
|
|
447
728
|
}
|
|
448
729
|
|
|
449
730
|
// src/profile.ts
|
|
450
|
-
import * as
|
|
451
|
-
import * as
|
|
731
|
+
import * as fs7 from "fs";
|
|
732
|
+
import * as path6 from "path";
|
|
452
733
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
453
734
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
454
735
|
var ProfileError = class extends Error {
|
|
@@ -461,12 +742,12 @@ var ProfileError = class extends Error {
|
|
|
461
742
|
profilePath;
|
|
462
743
|
};
|
|
463
744
|
function loadProfile(profilePath) {
|
|
464
|
-
if (!
|
|
745
|
+
if (!fs7.existsSync(profilePath)) {
|
|
465
746
|
throw new ProfileError(profilePath, "file not found");
|
|
466
747
|
}
|
|
467
748
|
let raw;
|
|
468
749
|
try {
|
|
469
|
-
raw = JSON.parse(
|
|
750
|
+
raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
|
|
470
751
|
} catch (err) {
|
|
471
752
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
472
753
|
}
|
|
@@ -490,7 +771,7 @@ function loadProfile(profilePath) {
|
|
|
490
771
|
outputContract: r.outputContract,
|
|
491
772
|
inputArtifacts: parseInputArtifacts(profilePath, r.input),
|
|
492
773
|
outputArtifacts: parseOutputArtifacts(profilePath, r.output),
|
|
493
|
-
dir:
|
|
774
|
+
dir: path6.dirname(profilePath)
|
|
494
775
|
};
|
|
495
776
|
return profile;
|
|
496
777
|
}
|
|
@@ -669,21 +950,21 @@ function parseScriptList(p, key, raw) {
|
|
|
669
950
|
}
|
|
670
951
|
|
|
671
952
|
// src/scripts/buildSyntheticPlugin.ts
|
|
672
|
-
import * as
|
|
953
|
+
import * as fs8 from "fs";
|
|
673
954
|
import * as os2 from "os";
|
|
674
|
-
import * as
|
|
955
|
+
import * as path7 from "path";
|
|
675
956
|
function getPluginsCatalogRoot() {
|
|
676
|
-
const here =
|
|
957
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
677
958
|
const candidates = [
|
|
678
|
-
|
|
959
|
+
path7.join(here, "..", "plugins"),
|
|
679
960
|
// dev: src/scripts → src/plugins
|
|
680
|
-
|
|
961
|
+
path7.join(here, "..", "..", "plugins"),
|
|
681
962
|
// built: dist/scripts → dist/plugins
|
|
682
|
-
|
|
963
|
+
path7.join(here, "..", "..", "src", "plugins")
|
|
683
964
|
// fallback
|
|
684
965
|
];
|
|
685
966
|
for (const c of candidates) {
|
|
686
|
-
if (
|
|
967
|
+
if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
|
|
687
968
|
}
|
|
688
969
|
return candidates[0];
|
|
689
970
|
}
|
|
@@ -693,50 +974,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
693
974
|
if (!needsSynthetic) return;
|
|
694
975
|
const catalog = getPluginsCatalogRoot();
|
|
695
976
|
const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
696
|
-
const root =
|
|
697
|
-
|
|
977
|
+
const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
|
|
978
|
+
fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
|
|
698
979
|
if (cc.skills.length > 0) {
|
|
699
|
-
const dst =
|
|
700
|
-
|
|
980
|
+
const dst = path7.join(root, "skills");
|
|
981
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
701
982
|
for (const name of cc.skills) {
|
|
702
|
-
const src =
|
|
703
|
-
if (!
|
|
704
|
-
copyDir(src,
|
|
983
|
+
const src = path7.join(catalog, "skills", name);
|
|
984
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
|
|
985
|
+
copyDir(src, path7.join(dst, name));
|
|
705
986
|
}
|
|
706
987
|
}
|
|
707
988
|
if (cc.commands.length > 0) {
|
|
708
|
-
const dst =
|
|
709
|
-
|
|
989
|
+
const dst = path7.join(root, "commands");
|
|
990
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
710
991
|
for (const name of cc.commands) {
|
|
711
|
-
const src =
|
|
712
|
-
if (!
|
|
713
|
-
|
|
992
|
+
const src = path7.join(catalog, "commands", `${name}.md`);
|
|
993
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
|
|
994
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
714
995
|
}
|
|
715
996
|
}
|
|
716
997
|
if (cc.subagents.length > 0) {
|
|
717
|
-
const dst =
|
|
718
|
-
|
|
998
|
+
const dst = path7.join(root, "agents");
|
|
999
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
719
1000
|
for (const name of cc.subagents) {
|
|
720
|
-
const src =
|
|
721
|
-
if (!
|
|
722
|
-
|
|
1001
|
+
const src = path7.join(catalog, "agents", `${name}.md`);
|
|
1002
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
|
|
1003
|
+
fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
|
|
723
1004
|
}
|
|
724
1005
|
}
|
|
725
1006
|
if (cc.hooks.length > 0) {
|
|
726
|
-
const dst =
|
|
727
|
-
|
|
1007
|
+
const dst = path7.join(root, "hooks");
|
|
1008
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
728
1009
|
const merged = { hooks: {} };
|
|
729
1010
|
for (const name of cc.hooks) {
|
|
730
|
-
const src =
|
|
731
|
-
if (!
|
|
732
|
-
const parsed = JSON.parse(
|
|
1011
|
+
const src = path7.join(catalog, "hooks", `${name}.json`);
|
|
1012
|
+
if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
|
|
1013
|
+
const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
|
|
733
1014
|
for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
|
|
734
1015
|
if (!Array.isArray(entries)) continue;
|
|
735
1016
|
if (!merged.hooks[event]) merged.hooks[event] = [];
|
|
736
1017
|
merged.hooks[event].push(...entries);
|
|
737
1018
|
}
|
|
738
1019
|
}
|
|
739
|
-
|
|
1020
|
+
fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
|
|
740
1021
|
`);
|
|
741
1022
|
}
|
|
742
1023
|
const manifest = {
|
|
@@ -747,17 +1028,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
|
|
|
747
1028
|
if (cc.skills.length > 0) manifest.skills = ["./skills/"];
|
|
748
1029
|
if (cc.commands.length > 0) manifest.commands = ["./commands/"];
|
|
749
1030
|
if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
|
|
750
|
-
|
|
1031
|
+
fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
|
|
751
1032
|
`);
|
|
752
1033
|
ctx.data.syntheticPluginPath = root;
|
|
753
1034
|
};
|
|
754
1035
|
function copyDir(src, dst) {
|
|
755
|
-
|
|
756
|
-
for (const ent of
|
|
757
|
-
const s =
|
|
758
|
-
const d =
|
|
1036
|
+
fs8.mkdirSync(dst, { recursive: true });
|
|
1037
|
+
for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
|
|
1038
|
+
const s = path7.join(src, ent.name);
|
|
1039
|
+
const d = path7.join(dst, ent.name);
|
|
759
1040
|
if (ent.isDirectory()) copyDir(s, d);
|
|
760
|
-
else if (ent.isFile())
|
|
1041
|
+
else if (ent.isFile()) fs8.copyFileSync(s, d);
|
|
761
1042
|
}
|
|
762
1043
|
}
|
|
763
1044
|
|
|
@@ -823,18 +1104,18 @@ function formatMissesForFeedback(misses) {
|
|
|
823
1104
|
}
|
|
824
1105
|
|
|
825
1106
|
// src/prompt.ts
|
|
826
|
-
import * as
|
|
827
|
-
import * as
|
|
1107
|
+
import * as fs9 from "fs";
|
|
1108
|
+
import * as path8 from "path";
|
|
828
1109
|
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
829
1110
|
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
830
1111
|
function loadProjectConventions(projectDir) {
|
|
831
1112
|
const out = [];
|
|
832
1113
|
for (const rel of CONVENTION_FILES) {
|
|
833
|
-
const abs =
|
|
834
|
-
if (!
|
|
1114
|
+
const abs = path8.join(projectDir, rel);
|
|
1115
|
+
if (!fs9.existsSync(abs)) continue;
|
|
835
1116
|
let content;
|
|
836
1117
|
try {
|
|
837
|
-
content =
|
|
1118
|
+
content = fs9.readFileSync(abs, "utf-8");
|
|
838
1119
|
} catch {
|
|
839
1120
|
continue;
|
|
840
1121
|
}
|
|
@@ -848,23 +1129,49 @@ function loadProjectConventions(projectDir) {
|
|
|
848
1129
|
}
|
|
849
1130
|
function parseAgentResult(finalText) {
|
|
850
1131
|
const text = (finalText || "").trim();
|
|
851
|
-
if (!text)
|
|
1132
|
+
if (!text)
|
|
1133
|
+
return {
|
|
1134
|
+
done: false,
|
|
1135
|
+
commitMessage: "",
|
|
1136
|
+
prSummary: "",
|
|
1137
|
+
feedbackActions: "",
|
|
1138
|
+
failureReason: "agent produced no final message"
|
|
1139
|
+
};
|
|
852
1140
|
const failedMatch = text.match(/(?:^|\n)\s*FAILED\s*:\s*(.+?)\s*$/s);
|
|
853
1141
|
if (failedMatch) {
|
|
854
|
-
return { done: false, commitMessage: "", prSummary: "", failureReason: failedMatch[1].trim() };
|
|
1142
|
+
return { done: false, commitMessage: "", prSummary: "", feedbackActions: "", failureReason: failedMatch[1].trim() };
|
|
855
1143
|
}
|
|
856
1144
|
if (!/(^|\n)\s*DONE\b/i.test(text)) {
|
|
857
|
-
return {
|
|
1145
|
+
return {
|
|
1146
|
+
done: false,
|
|
1147
|
+
commitMessage: "",
|
|
1148
|
+
prSummary: "",
|
|
1149
|
+
feedbackActions: "",
|
|
1150
|
+
failureReason: "no DONE or FAILED marker in agent output"
|
|
1151
|
+
};
|
|
858
1152
|
}
|
|
859
1153
|
const commitMatch = text.match(/^[ \t]*COMMIT_MSG\s*:\s*(.+)$/im);
|
|
860
1154
|
const commitMessage = commitMatch ? commitMatch[1].trim() : "";
|
|
1155
|
+
const feedbackActions = extractBlock(
|
|
1156
|
+
text,
|
|
1157
|
+
/(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
|
|
1158
|
+
/(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY)\s*:/i
|
|
1159
|
+
);
|
|
861
1160
|
const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
|
|
862
1161
|
let prSummary = "";
|
|
863
1162
|
if (summaryStart !== -1) {
|
|
864
1163
|
const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
|
|
865
1164
|
prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
|
|
866
1165
|
}
|
|
867
|
-
return { done: true, commitMessage, prSummary, failureReason: "" };
|
|
1166
|
+
return { done: true, commitMessage, prSummary, feedbackActions, failureReason: "" };
|
|
1167
|
+
}
|
|
1168
|
+
function extractBlock(text, startMarker, endMarker) {
|
|
1169
|
+
const startIdx = text.search(startMarker);
|
|
1170
|
+
if (startIdx === -1) return "";
|
|
1171
|
+
const afterStart = text.slice(startIdx).replace(startMarker, "");
|
|
1172
|
+
const endIdx = afterStart.search(endMarker);
|
|
1173
|
+
const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
|
|
1174
|
+
return body.replace(/\n\s*```\s*$/g, "").trim();
|
|
868
1175
|
}
|
|
869
1176
|
|
|
870
1177
|
// src/scripts/checkCoverageWithRetry.ts
|
|
@@ -911,8 +1218,8 @@ import { execFileSync as execFileSync4 } from "child_process";
|
|
|
911
1218
|
|
|
912
1219
|
// src/commit.ts
|
|
913
1220
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
914
|
-
import * as
|
|
915
|
-
import * as
|
|
1221
|
+
import * as fs10 from "fs";
|
|
1222
|
+
import * as path9 from "path";
|
|
916
1223
|
var FORBIDDEN_PATH_PREFIXES = [
|
|
917
1224
|
".kody/",
|
|
918
1225
|
".kody-engine/",
|
|
@@ -967,18 +1274,18 @@ function tryGit(args, cwd) {
|
|
|
967
1274
|
}
|
|
968
1275
|
function abortUnfinishedGitOps(cwd) {
|
|
969
1276
|
const aborted = [];
|
|
970
|
-
const gitDir =
|
|
971
|
-
if (!
|
|
972
|
-
if (
|
|
1277
|
+
const gitDir = path9.join(cwd ?? process.cwd(), ".git");
|
|
1278
|
+
if (!fs10.existsSync(gitDir)) return aborted;
|
|
1279
|
+
if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
|
|
973
1280
|
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
974
1281
|
}
|
|
975
|
-
if (
|
|
1282
|
+
if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
976
1283
|
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
977
1284
|
}
|
|
978
|
-
if (
|
|
1285
|
+
if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
|
|
979
1286
|
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
980
1287
|
}
|
|
981
|
-
if (
|
|
1288
|
+
if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
|
|
982
1289
|
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
983
1290
|
}
|
|
984
1291
|
try {
|
|
@@ -1020,7 +1327,7 @@ function normalizeCommitMessage(raw) {
|
|
|
1020
1327
|
function commitAndPush(branch, agentMessage, cwd) {
|
|
1021
1328
|
const allChanged = listChangedFiles(cwd);
|
|
1022
1329
|
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
1023
|
-
const mergeHeadExists =
|
|
1330
|
+
const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
1024
1331
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
1025
1332
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
1026
1333
|
}
|
|
@@ -1110,20 +1417,20 @@ function defaultCommitMessage(mode, data) {
|
|
|
1110
1417
|
}
|
|
1111
1418
|
|
|
1112
1419
|
// src/scripts/composePrompt.ts
|
|
1113
|
-
import * as
|
|
1114
|
-
import * as
|
|
1420
|
+
import * as fs11 from "fs";
|
|
1421
|
+
import * as path10 from "path";
|
|
1115
1422
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
1116
1423
|
var composePrompt = async (ctx, profile) => {
|
|
1117
1424
|
const explicit = ctx.data.promptTemplate;
|
|
1118
1425
|
const mode = ctx.args.mode;
|
|
1119
1426
|
const candidates = [
|
|
1120
|
-
explicit ?
|
|
1121
|
-
mode ?
|
|
1122
|
-
|
|
1427
|
+
explicit ? path10.join(profile.dir, explicit) : null,
|
|
1428
|
+
mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
1429
|
+
path10.join(profile.dir, "prompt.md")
|
|
1123
1430
|
].filter(Boolean);
|
|
1124
1431
|
let templatePath = "";
|
|
1125
1432
|
for (const c of candidates) {
|
|
1126
|
-
if (
|
|
1433
|
+
if (fs11.existsSync(c)) {
|
|
1127
1434
|
templatePath = c;
|
|
1128
1435
|
break;
|
|
1129
1436
|
}
|
|
@@ -1131,7 +1438,7 @@ var composePrompt = async (ctx, profile) => {
|
|
|
1131
1438
|
if (!templatePath) {
|
|
1132
1439
|
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
1133
1440
|
}
|
|
1134
|
-
const template =
|
|
1441
|
+
const template = fs11.readFileSync(templatePath, "utf-8");
|
|
1135
1442
|
const tokens = {
|
|
1136
1443
|
...stringifyAll(ctx.args, "args."),
|
|
1137
1444
|
...stringifyAll(ctx.data, ""),
|
|
@@ -1603,7 +1910,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
1603
1910
|
|
|
1604
1911
|
// src/gha.ts
|
|
1605
1912
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1606
|
-
import * as
|
|
1913
|
+
import * as fs12 from "fs";
|
|
1607
1914
|
function getRunUrl() {
|
|
1608
1915
|
const server = process.env.GITHUB_SERVER_URL;
|
|
1609
1916
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -1614,10 +1921,10 @@ function getRunUrl() {
|
|
|
1614
1921
|
function reactToTriggerComment(cwd) {
|
|
1615
1922
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
1616
1923
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1617
|
-
if (!eventPath || !
|
|
1924
|
+
if (!eventPath || !fs12.existsSync(eventPath)) return;
|
|
1618
1925
|
let event = null;
|
|
1619
1926
|
try {
|
|
1620
|
-
event = JSON.parse(
|
|
1927
|
+
event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
|
|
1621
1928
|
} catch {
|
|
1622
1929
|
return;
|
|
1623
1930
|
}
|
|
@@ -1843,35 +2150,35 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
1843
2150
|
|
|
1844
2151
|
// src/scripts/initFlow.ts
|
|
1845
2152
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
1846
|
-
import * as
|
|
1847
|
-
import * as
|
|
2153
|
+
import * as fs14 from "fs";
|
|
2154
|
+
import * as path12 from "path";
|
|
1848
2155
|
|
|
1849
2156
|
// src/registry.ts
|
|
1850
|
-
import * as
|
|
1851
|
-
import * as
|
|
2157
|
+
import * as fs13 from "fs";
|
|
2158
|
+
import * as path11 from "path";
|
|
1852
2159
|
function getExecutablesRoot() {
|
|
1853
|
-
const here =
|
|
2160
|
+
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
1854
2161
|
const candidates = [
|
|
1855
|
-
|
|
2162
|
+
path11.join(here, "executables"),
|
|
1856
2163
|
// dev: src/
|
|
1857
|
-
|
|
2164
|
+
path11.join(here, "..", "executables"),
|
|
1858
2165
|
// built: dist/bin → dist/executables
|
|
1859
|
-
|
|
2166
|
+
path11.join(here, "..", "src", "executables")
|
|
1860
2167
|
// fallback
|
|
1861
2168
|
];
|
|
1862
2169
|
for (const c of candidates) {
|
|
1863
|
-
if (
|
|
2170
|
+
if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
|
|
1864
2171
|
}
|
|
1865
2172
|
return candidates[0];
|
|
1866
2173
|
}
|
|
1867
2174
|
function listExecutables(root = getExecutablesRoot()) {
|
|
1868
|
-
if (!
|
|
1869
|
-
const entries =
|
|
2175
|
+
if (!fs13.existsSync(root)) return [];
|
|
2176
|
+
const entries = fs13.readdirSync(root, { withFileTypes: true });
|
|
1870
2177
|
const out = [];
|
|
1871
2178
|
for (const ent of entries) {
|
|
1872
2179
|
if (!ent.isDirectory()) continue;
|
|
1873
|
-
const profilePath =
|
|
1874
|
-
if (
|
|
2180
|
+
const profilePath = path11.join(root, ent.name, "profile.json");
|
|
2181
|
+
if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
|
|
1875
2182
|
out.push({ name: ent.name, profilePath });
|
|
1876
2183
|
}
|
|
1877
2184
|
}
|
|
@@ -1879,8 +2186,8 @@ function listExecutables(root = getExecutablesRoot()) {
|
|
|
1879
2186
|
}
|
|
1880
2187
|
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
1881
2188
|
if (!isSafeName(name)) return false;
|
|
1882
|
-
const profilePath =
|
|
1883
|
-
return
|
|
2189
|
+
const profilePath = path11.join(root, name, "profile.json");
|
|
2190
|
+
return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
|
|
1884
2191
|
}
|
|
1885
2192
|
function isSafeName(name) {
|
|
1886
2193
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
@@ -1909,9 +2216,9 @@ function parseGenericFlags(argv) {
|
|
|
1909
2216
|
|
|
1910
2217
|
// src/scripts/initFlow.ts
|
|
1911
2218
|
function detectPackageManager(cwd) {
|
|
1912
|
-
if (
|
|
1913
|
-
if (
|
|
1914
|
-
if (
|
|
2219
|
+
if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2220
|
+
if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
|
|
2221
|
+
if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
|
|
1915
2222
|
return "npm";
|
|
1916
2223
|
}
|
|
1917
2224
|
function qualityCommandsFor(pm) {
|
|
@@ -2032,22 +2339,22 @@ function performInit(cwd, force) {
|
|
|
2032
2339
|
const pm = detectPackageManager(cwd);
|
|
2033
2340
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
2034
2341
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
2035
|
-
const configPath =
|
|
2036
|
-
if (
|
|
2342
|
+
const configPath = path12.join(cwd, "kody.config.json");
|
|
2343
|
+
if (fs14.existsSync(configPath) && !force) {
|
|
2037
2344
|
skipped.push("kody.config.json");
|
|
2038
2345
|
} else {
|
|
2039
2346
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
2040
|
-
|
|
2347
|
+
fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
2041
2348
|
`);
|
|
2042
2349
|
wrote.push("kody.config.json");
|
|
2043
2350
|
}
|
|
2044
|
-
const workflowDir =
|
|
2045
|
-
const workflowPath =
|
|
2046
|
-
if (
|
|
2351
|
+
const workflowDir = path12.join(cwd, ".github", "workflows");
|
|
2352
|
+
const workflowPath = path12.join(workflowDir, "kody2.yml");
|
|
2353
|
+
if (fs14.existsSync(workflowPath) && !force) {
|
|
2047
2354
|
skipped.push(".github/workflows/kody2.yml");
|
|
2048
2355
|
} else {
|
|
2049
|
-
|
|
2050
|
-
|
|
2356
|
+
fs14.mkdirSync(workflowDir, { recursive: true });
|
|
2357
|
+
fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
2051
2358
|
wrote.push(".github/workflows/kody2.yml");
|
|
2052
2359
|
}
|
|
2053
2360
|
for (const exe of listExecutables()) {
|
|
@@ -2058,12 +2365,12 @@ function performInit(cwd, force) {
|
|
|
2058
2365
|
continue;
|
|
2059
2366
|
}
|
|
2060
2367
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
2061
|
-
const target =
|
|
2062
|
-
if (
|
|
2368
|
+
const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
|
|
2369
|
+
if (fs14.existsSync(target) && !force) {
|
|
2063
2370
|
skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2064
2371
|
continue;
|
|
2065
2372
|
}
|
|
2066
|
-
|
|
2373
|
+
fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
2067
2374
|
wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
|
|
2068
2375
|
}
|
|
2069
2376
|
return { wrote, skipped };
|
|
@@ -2277,17 +2584,19 @@ function renderStateComment(state) {
|
|
|
2277
2584
|
lines.push(STATE_BEGIN);
|
|
2278
2585
|
lines.push("");
|
|
2279
2586
|
lines.push("```json");
|
|
2280
|
-
lines.push(
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2587
|
+
lines.push(
|
|
2588
|
+
JSON.stringify(
|
|
2589
|
+
{
|
|
2590
|
+
schemaVersion: state.schemaVersion,
|
|
2591
|
+
core: state.core,
|
|
2592
|
+
artifacts: state.artifacts ?? {},
|
|
2593
|
+
executables: state.executables,
|
|
2594
|
+
history: state.history
|
|
2595
|
+
},
|
|
2596
|
+
null,
|
|
2597
|
+
2
|
|
2598
|
+
)
|
|
2599
|
+
);
|
|
2291
2600
|
lines.push("```");
|
|
2292
2601
|
lines.push("");
|
|
2293
2602
|
lines.push(STATE_END);
|
|
@@ -2337,11 +2646,7 @@ function writeTaskState(target, number, state, cwd) {
|
|
|
2337
2646
|
const existing = findStateComment(target, number, cwd);
|
|
2338
2647
|
try {
|
|
2339
2648
|
if (existing) {
|
|
2340
|
-
gh3(
|
|
2341
|
-
["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"],
|
|
2342
|
-
body,
|
|
2343
|
-
cwd
|
|
2344
|
-
);
|
|
2649
|
+
gh3(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
|
|
2345
2650
|
} else {
|
|
2346
2651
|
const sub = target === "issue" ? "issue" : "pr";
|
|
2347
2652
|
gh3([sub, "comment", String(number), "--body-file", "-"], body, cwd);
|
|
@@ -2376,6 +2681,7 @@ var parseAgentResult2 = async (ctx, profile, agentResult) => {
|
|
|
2376
2681
|
ctx.data.agentDone = parsed.done;
|
|
2377
2682
|
ctx.data.commitMessage = parsed.commitMessage;
|
|
2378
2683
|
ctx.data.prSummary = parsed.prSummary;
|
|
2684
|
+
ctx.data.feedbackActions = parsed.feedbackActions;
|
|
2379
2685
|
ctx.data.agentFailureReason = parsed.failureReason;
|
|
2380
2686
|
ctx.data.agentOutcome = agentResult.outcome;
|
|
2381
2687
|
ctx.data.agentError = agentResult.error;
|
|
@@ -2545,15 +2851,17 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
|
|
|
2545
2851
|
const verdict = detectVerdict(reviewBody);
|
|
2546
2852
|
ctx.data.reviewVerdict = verdict;
|
|
2547
2853
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
2548
|
-
process.stdout.write(
|
|
2854
|
+
process.stdout.write(
|
|
2855
|
+
`
|
|
2549
2856
|
REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
|
|
2550
|
-
`
|
|
2857
|
+
`
|
|
2858
|
+
);
|
|
2551
2859
|
};
|
|
2552
2860
|
|
|
2553
2861
|
// src/scripts/releaseFlow.ts
|
|
2554
2862
|
import { execFileSync as execFileSync11, spawnSync } from "child_process";
|
|
2555
|
-
import * as
|
|
2556
|
-
import * as
|
|
2863
|
+
import * as fs15 from "fs";
|
|
2864
|
+
import * as path13 from "path";
|
|
2557
2865
|
function bumpVersion(current, bump) {
|
|
2558
2866
|
const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
2559
2867
|
if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
|
|
@@ -2569,12 +2877,12 @@ function bumpVersion(current, bump) {
|
|
|
2569
2877
|
return `${major}.${minor}.${patch}`;
|
|
2570
2878
|
}
|
|
2571
2879
|
function updateVersionInFile(file, newVersion, cwd) {
|
|
2572
|
-
const abs =
|
|
2573
|
-
if (!
|
|
2574
|
-
const content =
|
|
2880
|
+
const abs = path13.join(cwd, file);
|
|
2881
|
+
if (!fs15.existsSync(abs)) return false;
|
|
2882
|
+
const content = fs15.readFileSync(abs, "utf-8");
|
|
2575
2883
|
const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
|
|
2576
2884
|
if (updated === content) return false;
|
|
2577
|
-
|
|
2885
|
+
fs15.writeFileSync(abs, updated);
|
|
2578
2886
|
return true;
|
|
2579
2887
|
}
|
|
2580
2888
|
function generateChangelog(cwd, newVersion, lastTag) {
|
|
@@ -2622,19 +2930,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
|
|
|
2622
2930
|
return parts.join("\n");
|
|
2623
2931
|
}
|
|
2624
2932
|
function prependChangelog(cwd, entry) {
|
|
2625
|
-
const p =
|
|
2933
|
+
const p = path13.join(cwd, "CHANGELOG.md");
|
|
2626
2934
|
const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
|
|
2627
|
-
if (
|
|
2628
|
-
const prior =
|
|
2935
|
+
if (fs15.existsSync(p)) {
|
|
2936
|
+
const prior = fs15.readFileSync(p, "utf-8");
|
|
2629
2937
|
if (/^#\s*Changelog\b/m.test(prior)) {
|
|
2630
2938
|
const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
|
|
2631
|
-
|
|
2939
|
+
fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
|
|
2632
2940
|
${entry}${prior.slice(idx + 1)}`);
|
|
2633
2941
|
} else {
|
|
2634
|
-
|
|
2942
|
+
fs15.writeFileSync(p, `${header}${entry}${prior}`);
|
|
2635
2943
|
}
|
|
2636
2944
|
} else {
|
|
2637
|
-
|
|
2945
|
+
fs15.writeFileSync(p, `${header}${entry}`);
|
|
2638
2946
|
}
|
|
2639
2947
|
}
|
|
2640
2948
|
function git3(args, cwd, timeout = 6e4) {
|
|
@@ -2685,13 +2993,13 @@ var releaseFlow = async (ctx) => {
|
|
|
2685
2993
|
};
|
|
2686
2994
|
async function runPrepare(args) {
|
|
2687
2995
|
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
2688
|
-
const pkgPath =
|
|
2689
|
-
if (!
|
|
2996
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
2997
|
+
if (!fs15.existsSync(pkgPath)) {
|
|
2690
2998
|
ctx.output.exitCode = 99;
|
|
2691
2999
|
ctx.output.reason = "release prepare: package.json not found";
|
|
2692
3000
|
return;
|
|
2693
3001
|
}
|
|
2694
|
-
const pkg = JSON.parse(
|
|
3002
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2695
3003
|
if (typeof pkg.version !== "string") {
|
|
2696
3004
|
ctx.output.exitCode = 99;
|
|
2697
3005
|
ctx.output.reason = "release prepare: package.json has no version";
|
|
@@ -2745,10 +3053,10 @@ ${entry}
|
|
|
2745
3053
|
Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
2746
3054
|
let prUrl = "";
|
|
2747
3055
|
try {
|
|
2748
|
-
prUrl = gh(
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
).trim();
|
|
3056
|
+
prUrl = gh(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
|
|
3057
|
+
input: body,
|
|
3058
|
+
cwd
|
|
3059
|
+
}).trim();
|
|
2752
3060
|
} catch (err) {
|
|
2753
3061
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2754
3062
|
ctx.output.exitCode = 4;
|
|
@@ -2762,8 +3070,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
|
|
|
2762
3070
|
}
|
|
2763
3071
|
async function runFinalize(args) {
|
|
2764
3072
|
const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
|
|
2765
|
-
const pkgPath =
|
|
2766
|
-
const pkg = JSON.parse(
|
|
3073
|
+
const pkgPath = path13.join(cwd, "package.json");
|
|
3074
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2767
3075
|
if (typeof pkg.version !== "string") {
|
|
2768
3076
|
ctx.output.exitCode = 99;
|
|
2769
3077
|
ctx.output.reason = "release finalize: package.json has no version";
|
|
@@ -2820,20 +3128,14 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
2820
3128
|
}
|
|
2821
3129
|
let releaseUrl = "";
|
|
2822
3130
|
try {
|
|
2823
|
-
const releaseArgs = [
|
|
2824
|
-
"release",
|
|
2825
|
-
"create",
|
|
2826
|
-
tag,
|
|
2827
|
-
"--title",
|
|
2828
|
-
tag,
|
|
2829
|
-
"--notes",
|
|
2830
|
-
`Release ${tag} \u2014 automated by kody2.`
|
|
2831
|
-
];
|
|
3131
|
+
const releaseArgs = ["release", "create", tag, "--title", tag, "--notes", `Release ${tag} \u2014 automated by kody2.`];
|
|
2832
3132
|
if (releaseCfg.draftRelease) releaseArgs.push("--draft");
|
|
2833
3133
|
releaseUrl = gh(releaseArgs, { cwd }).trim();
|
|
2834
3134
|
} catch (err) {
|
|
2835
|
-
process.stderr.write(
|
|
2836
|
-
`)
|
|
3135
|
+
process.stderr.write(
|
|
3136
|
+
`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
|
|
3137
|
+
`
|
|
3138
|
+
);
|
|
2837
3139
|
}
|
|
2838
3140
|
if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
|
|
2839
3141
|
const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
|
|
@@ -2852,6 +3154,35 @@ ${truncate2(r.stderr, 2e3)}
|
|
|
2852
3154
|
`);
|
|
2853
3155
|
}
|
|
2854
3156
|
|
|
3157
|
+
// src/scripts/requireFeedbackActions.ts
|
|
3158
|
+
var MIN_ITEMS = 1;
|
|
3159
|
+
var requireFeedbackActions = async (ctx, profile) => {
|
|
3160
|
+
if (!ctx.data.agentDone) return;
|
|
3161
|
+
const actions = String(ctx.data.feedbackActions ?? "").trim();
|
|
3162
|
+
const items = countActionItems(actions);
|
|
3163
|
+
if (items >= MIN_ITEMS) return;
|
|
3164
|
+
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";
|
|
3165
|
+
ctx.data.agentDone = false;
|
|
3166
|
+
ctx.data.agentFailureReason = reason;
|
|
3167
|
+
const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
|
|
3168
|
+
const failedAction = {
|
|
3169
|
+
type: `${modeSeg}_FAILED`,
|
|
3170
|
+
payload: { reason },
|
|
3171
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3172
|
+
};
|
|
3173
|
+
ctx.data.action = failedAction;
|
|
3174
|
+
};
|
|
3175
|
+
function countActionItems(block) {
|
|
3176
|
+
if (!block.trim()) return 0;
|
|
3177
|
+
const lines = block.split("\n");
|
|
3178
|
+
let count = 0;
|
|
3179
|
+
for (const raw of lines) {
|
|
3180
|
+
const line = raw.trim();
|
|
3181
|
+
if (/^[-*]\s+/.test(line)) count++;
|
|
3182
|
+
}
|
|
3183
|
+
return count;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
2855
3186
|
// src/scripts/resolveArtifacts.ts
|
|
2856
3187
|
var resolveArtifacts = async (ctx, profile) => {
|
|
2857
3188
|
if (profile.inputArtifacts.length === 0) return;
|
|
@@ -3101,11 +3432,7 @@ var syncFlow = async (ctx) => {
|
|
|
3101
3432
|
ctx.output.reason = `merged origin/${baseBranch} into ${ctx.data.branch}`;
|
|
3102
3433
|
const runUrl = getRunUrl();
|
|
3103
3434
|
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
|
-
);
|
|
3435
|
+
tryPostPr5(prNumber, `\u2705 kody2 sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`, ctx.cwd);
|
|
3109
3436
|
};
|
|
3110
3437
|
function bail2(ctx, prNumber, reason) {
|
|
3111
3438
|
ctx.output.exitCode = 1;
|
|
@@ -3145,7 +3472,7 @@ import { spawn as spawn2 } from "child_process";
|
|
|
3145
3472
|
var TAIL_CHARS = 4e3;
|
|
3146
3473
|
var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3147
3474
|
function runCommand(command, cwd) {
|
|
3148
|
-
return new Promise((
|
|
3475
|
+
return new Promise((resolve4) => {
|
|
3149
3476
|
const start = Date.now();
|
|
3150
3477
|
const child = spawn2(command, {
|
|
3151
3478
|
cwd,
|
|
@@ -3174,11 +3501,11 @@ function runCommand(command, cwd) {
|
|
|
3174
3501
|
child.on("exit", (code) => {
|
|
3175
3502
|
clearTimeout(timer);
|
|
3176
3503
|
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
3177
|
-
|
|
3504
|
+
resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
3178
3505
|
});
|
|
3179
3506
|
child.on("error", (err) => {
|
|
3180
3507
|
clearTimeout(timer);
|
|
3181
|
-
|
|
3508
|
+
resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
3182
3509
|
});
|
|
3183
3510
|
});
|
|
3184
3511
|
}
|
|
@@ -3237,19 +3564,7 @@ function readWatchConfig(ctx) {
|
|
|
3237
3564
|
function findStalePrs(cwd, staleDays, now = /* @__PURE__ */ new Date()) {
|
|
3238
3565
|
let raw = "";
|
|
3239
3566
|
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
|
-
);
|
|
3567
|
+
raw = gh(["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,url,updatedAt"], { cwd });
|
|
3253
3568
|
} catch {
|
|
3254
3569
|
return [];
|
|
3255
3570
|
}
|
|
@@ -3295,8 +3610,10 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3295
3610
|
try {
|
|
3296
3611
|
postIssueComment(reportIssueNumber, report, ctx.cwd);
|
|
3297
3612
|
} catch (err) {
|
|
3298
|
-
process.stderr.write(
|
|
3299
|
-
`)
|
|
3613
|
+
process.stderr.write(
|
|
3614
|
+
`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
3615
|
+
`
|
|
3616
|
+
);
|
|
3300
3617
|
}
|
|
3301
3618
|
}
|
|
3302
3619
|
ctx.output.exitCode = 0;
|
|
@@ -3304,7 +3621,7 @@ var watchStalePrsFlow = async (ctx) => {
|
|
|
3304
3621
|
};
|
|
3305
3622
|
|
|
3306
3623
|
// src/scripts/writeRunSummary.ts
|
|
3307
|
-
import * as
|
|
3624
|
+
import * as fs16 from "fs";
|
|
3308
3625
|
var writeRunSummary = async (ctx, profile) => {
|
|
3309
3626
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
3310
3627
|
if (!summaryPath) return;
|
|
@@ -3326,7 +3643,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
3326
3643
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
3327
3644
|
lines.push("");
|
|
3328
3645
|
try {
|
|
3329
|
-
|
|
3646
|
+
fs16.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
3330
3647
|
`);
|
|
3331
3648
|
} catch {
|
|
3332
3649
|
}
|
|
@@ -3353,6 +3670,7 @@ var preflightScripts = {
|
|
|
3353
3670
|
};
|
|
3354
3671
|
var postflightScripts = {
|
|
3355
3672
|
parseAgentResult: parseAgentResult2,
|
|
3673
|
+
requireFeedbackActions,
|
|
3356
3674
|
verify,
|
|
3357
3675
|
checkCoverageWithRetry,
|
|
3358
3676
|
commitAndPush: commitAndPush2,
|
|
@@ -3471,9 +3789,9 @@ async function runExecutable(profileName, input) {
|
|
|
3471
3789
|
data: {},
|
|
3472
3790
|
output: { exitCode: 0 }
|
|
3473
3791
|
};
|
|
3474
|
-
const ndjsonDir =
|
|
3792
|
+
const ndjsonDir = path14.join(input.cwd, ".kody2");
|
|
3475
3793
|
const invokeAgent = async (prompt) => {
|
|
3476
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
3794
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
3477
3795
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
3478
3796
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
3479
3797
|
return runAgent({
|
|
@@ -3539,17 +3857,17 @@ async function runExecutable(profileName, input) {
|
|
|
3539
3857
|
}
|
|
3540
3858
|
}
|
|
3541
3859
|
function resolveProfilePath(profileName) {
|
|
3542
|
-
const here =
|
|
3860
|
+
const here = path14.dirname(new URL(import.meta.url).pathname);
|
|
3543
3861
|
const candidates = [
|
|
3544
|
-
|
|
3862
|
+
path14.join(here, "executables", profileName, "profile.json"),
|
|
3545
3863
|
// same-dir sibling (dev)
|
|
3546
|
-
|
|
3864
|
+
path14.join(here, "..", "executables", profileName, "profile.json"),
|
|
3547
3865
|
// up one (prod: dist/bin → dist/executables)
|
|
3548
|
-
|
|
3866
|
+
path14.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
3549
3867
|
// fallback
|
|
3550
3868
|
];
|
|
3551
3869
|
for (const c of candidates) {
|
|
3552
|
-
if (
|
|
3870
|
+
if (fs17.existsSync(c)) return c;
|
|
3553
3871
|
}
|
|
3554
3872
|
return candidates[0];
|
|
3555
3873
|
}
|
|
@@ -3640,95 +3958,6 @@ function finish(out) {
|
|
|
3640
3958
|
return out;
|
|
3641
3959
|
}
|
|
3642
3960
|
|
|
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
3961
|
// src/kody2-cli.ts
|
|
3733
3962
|
var CI_HELP = `kody2 ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
|
|
3734
3963
|
|
|
@@ -3814,9 +4043,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
3814
4043
|
return token;
|
|
3815
4044
|
}
|
|
3816
4045
|
function detectPackageManager2(cwd) {
|
|
3817
|
-
if (
|
|
3818
|
-
if (
|
|
3819
|
-
if (
|
|
4046
|
+
if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4047
|
+
if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
|
|
4048
|
+
if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
|
|
3820
4049
|
return "npm";
|
|
3821
4050
|
}
|
|
3822
4051
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -3896,11 +4125,11 @@ function configureGitIdentity(cwd) {
|
|
|
3896
4125
|
}
|
|
3897
4126
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
3898
4127
|
if (!issueNumber) return;
|
|
3899
|
-
const logPath =
|
|
4128
|
+
const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
|
|
3900
4129
|
let tail = "";
|
|
3901
4130
|
try {
|
|
3902
|
-
if (
|
|
3903
|
-
const content =
|
|
4131
|
+
if (fs18.existsSync(logPath)) {
|
|
4132
|
+
const content = fs18.readFileSync(logPath, "utf-8");
|
|
3904
4133
|
tail = content.slice(-3e3);
|
|
3905
4134
|
}
|
|
3906
4135
|
} catch {
|
|
@@ -3925,7 +4154,7 @@ async function runCi(argv) {
|
|
|
3925
4154
|
return 0;
|
|
3926
4155
|
}
|
|
3927
4156
|
const args = parseCiArgs(argv);
|
|
3928
|
-
const cwd = args.cwd ?
|
|
4157
|
+
const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
|
|
3929
4158
|
let earlyConfig;
|
|
3930
4159
|
try {
|
|
3931
4160
|
earlyConfig = loadConfig(cwd);
|
|
@@ -4013,6 +4242,148 @@ ${CI_HELP}`);
|
|
|
4013
4242
|
}
|
|
4014
4243
|
}
|
|
4015
4244
|
|
|
4245
|
+
// src/chat-cli.ts
|
|
4246
|
+
var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
|
|
4247
|
+
var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
|
|
4248
|
+
|
|
4249
|
+
Usage:
|
|
4250
|
+
kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
|
|
4251
|
+
[--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
|
|
4252
|
+
|
|
4253
|
+
All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
|
|
4254
|
+
CLI flags take precedence over env. SESSION_ID is required.
|
|
4255
|
+
|
|
4256
|
+
Exit codes:
|
|
4257
|
+
0 reply emitted successfully
|
|
4258
|
+
64 bad inputs (missing session, empty history)
|
|
4259
|
+
99 runtime failure (agent crash, LiteLLM failure)
|
|
4260
|
+
`;
|
|
4261
|
+
function parseChatArgs(argv, env = process.env) {
|
|
4262
|
+
const result = { errors: [] };
|
|
4263
|
+
for (let i = 0; i < argv.length; i++) {
|
|
4264
|
+
const arg = argv[i];
|
|
4265
|
+
if (arg === "--session") result.sessionId = argv[++i];
|
|
4266
|
+
else if (arg === "--message") result.initMessage = argv[++i];
|
|
4267
|
+
else if (arg === "--model") result.model = argv[++i];
|
|
4268
|
+
else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
|
|
4269
|
+
else if (arg === "--cwd") result.cwd = argv[++i];
|
|
4270
|
+
else if (arg === "--verbose") result.verbose = true;
|
|
4271
|
+
else if (arg === "--quiet") result.quiet = true;
|
|
4272
|
+
else if (arg === "--help" || arg === "-h") result.errors.push("__HELP__");
|
|
4273
|
+
else if (arg?.startsWith("--")) result.errors.push(`unknown arg: ${arg}`);
|
|
4274
|
+
else if (arg) result.errors.push(`unexpected positional: ${arg}`);
|
|
4275
|
+
}
|
|
4276
|
+
result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
|
|
4277
|
+
result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
|
|
4278
|
+
result.model = result.model ?? env.MODEL ?? void 0;
|
|
4279
|
+
result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
|
|
4280
|
+
for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
|
|
4281
|
+
const v = result[key];
|
|
4282
|
+
if (typeof v === "string" && v.trim() === "") result[key] = void 0;
|
|
4283
|
+
}
|
|
4284
|
+
if (!result.sessionId && !result.errors.includes("__HELP__")) {
|
|
4285
|
+
result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
4286
|
+
}
|
|
4287
|
+
return result;
|
|
4288
|
+
}
|
|
4289
|
+
function commitChatFiles(cwd, sessionId, verbose) {
|
|
4290
|
+
const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
4291
|
+
const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
4292
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
|
|
4293
|
+
if (paths.length === 0) return;
|
|
4294
|
+
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
4295
|
+
try {
|
|
4296
|
+
execFileSync16("git", ["add", ...paths], opts);
|
|
4297
|
+
execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
|
|
4298
|
+
execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
|
|
4299
|
+
} catch (err) {
|
|
4300
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4301
|
+
process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
|
|
4302
|
+
`);
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
function tryLoadConfig(cwd) {
|
|
4306
|
+
try {
|
|
4307
|
+
return loadConfig(cwd);
|
|
4308
|
+
} catch {
|
|
4309
|
+
return null;
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
function buildSink(cwd, sessionId, dashboardUrl) {
|
|
4313
|
+
const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
|
|
4314
|
+
if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
|
|
4315
|
+
return new TeeSink(sinks);
|
|
4316
|
+
}
|
|
4317
|
+
async function runChat(argv) {
|
|
4318
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
4319
|
+
process.stdout.write(CHAT_HELP);
|
|
4320
|
+
return 0;
|
|
4321
|
+
}
|
|
4322
|
+
const args = parseChatArgs(argv);
|
|
4323
|
+
if (args.errors.length > 0 && !args.errors.includes("__HELP__")) {
|
|
4324
|
+
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
4325
|
+
`);
|
|
4326
|
+
process.stderr.write(`
|
|
4327
|
+
${CHAT_HELP}`);
|
|
4328
|
+
return 64;
|
|
4329
|
+
}
|
|
4330
|
+
const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
|
|
4331
|
+
const sessionId = args.sessionId;
|
|
4332
|
+
const unpackedSecrets = unpackAllSecrets();
|
|
4333
|
+
if (unpackedSecrets > 0) {
|
|
4334
|
+
process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
|
|
4335
|
+
`);
|
|
4336
|
+
}
|
|
4337
|
+
resolveAuthToken();
|
|
4338
|
+
configureGitIdentity(cwd);
|
|
4339
|
+
const config = tryLoadConfig(cwd);
|
|
4340
|
+
const modelSpec = args.model ?? config?.agent.model ?? DEFAULT_MODEL;
|
|
4341
|
+
let model;
|
|
4342
|
+
try {
|
|
4343
|
+
model = parseProviderModel(modelSpec);
|
|
4344
|
+
} catch (err) {
|
|
4345
|
+
process.stderr.write(`error: invalid model '${modelSpec}': ${err instanceof Error ? err.message : String(err)}
|
|
4346
|
+
`);
|
|
4347
|
+
return 64;
|
|
4348
|
+
}
|
|
4349
|
+
let litellm = null;
|
|
4350
|
+
try {
|
|
4351
|
+
litellm = await startLitellmIfNeeded(model, cwd);
|
|
4352
|
+
} catch (err) {
|
|
4353
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4354
|
+
const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4355
|
+
await sink2.emit({
|
|
4356
|
+
event: "chat.error",
|
|
4357
|
+
payload: { sessionId, error: `litellm startup failed: ${msg}` },
|
|
4358
|
+
runId: makeRunId(sessionId, "error"),
|
|
4359
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4360
|
+
});
|
|
4361
|
+
return 99;
|
|
4362
|
+
}
|
|
4363
|
+
const sessionFile = sessionFilePath(cwd, sessionId);
|
|
4364
|
+
if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
|
|
4365
|
+
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
4366
|
+
try {
|
|
4367
|
+
const result = await runChatTurn({
|
|
4368
|
+
sessionId,
|
|
4369
|
+
sessionFile,
|
|
4370
|
+
cwd,
|
|
4371
|
+
model,
|
|
4372
|
+
litellmUrl: litellm?.url ?? null,
|
|
4373
|
+
sink,
|
|
4374
|
+
verbose: args.verbose,
|
|
4375
|
+
quiet: args.quiet
|
|
4376
|
+
});
|
|
4377
|
+
commitChatFiles(cwd, sessionId, args.verbose ?? false);
|
|
4378
|
+
return result.exitCode;
|
|
4379
|
+
} finally {
|
|
4380
|
+
try {
|
|
4381
|
+
litellm?.kill();
|
|
4382
|
+
} catch {
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4016
4387
|
// src/entry.ts
|
|
4017
4388
|
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
4018
4389
|
|
|
@@ -4024,6 +4395,7 @@ Usage:
|
|
|
4024
4395
|
kody2 review --pr <N> [--cwd <path>] [--verbose|--quiet]
|
|
4025
4396
|
kody2 <other> [--cwd <path>] [--verbose|--quiet]
|
|
4026
4397
|
kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
|
|
4398
|
+
kody2 chat [chat flags \u2014 see: kody2 chat --help]
|
|
4027
4399
|
kody2 help
|
|
4028
4400
|
kody2 version
|
|
4029
4401
|
|
|
@@ -4050,6 +4422,9 @@ function parseArgs(argv) {
|
|
|
4050
4422
|
if (cmd === "ci") {
|
|
4051
4423
|
return { ...result, command: "ci", ciArgv: argv.slice(1) };
|
|
4052
4424
|
}
|
|
4425
|
+
if (cmd === "chat") {
|
|
4426
|
+
return { ...result, command: "chat", chatArgv: argv.slice(1) };
|
|
4427
|
+
}
|
|
4053
4428
|
if (hasExecutable(cmd)) {
|
|
4054
4429
|
result.command = "__executable__";
|
|
4055
4430
|
result.executableName = cmd;
|
|
@@ -4090,6 +4465,18 @@ ${HELP_TEXT}`);
|
|
|
4090
4465
|
process.stderr.write(`[kody2] fatal: ${msg}
|
|
4091
4466
|
`);
|
|
4092
4467
|
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
4468
|
+
`);
|
|
4469
|
+
return 99;
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
if (args.command === "chat") {
|
|
4473
|
+
try {
|
|
4474
|
+
return await runChat(args.chatArgv ?? []);
|
|
4475
|
+
} catch (err) {
|
|
4476
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4477
|
+
process.stderr.write(`[kody2] fatal: ${msg}
|
|
4478
|
+
`);
|
|
4479
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
4093
4480
|
`);
|
|
4094
4481
|
return 99;
|
|
4095
4482
|
}
|