@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 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.22",
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/executor.ts
53
- import * as fs14 from "fs";
54
- import * as path12 from "path";
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 fs2 from "fs";
58
- import * as path2 from "path";
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 fs from "fs";
63
- import * as path from "path";
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 = path.join(projectDir, "kody.config.json");
82
- if (!fs.existsSync(configPath)) {
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(fs.readFileSync(configPath, "utf-8"));
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 ?? path2.join(opts.cwd, ".kody2");
265
- fs2.mkdirSync(ndjsonDir, { recursive: true });
266
- const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
267
- const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
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 fs3 from "fs";
622
+ import * as fs6 from "fs";
342
623
  import * as os from "os";
343
- import * as path3 from "path";
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 = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
384
- fs3.writeFileSync(configPath, generateLitellmConfigYaml(model));
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 = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
390
- const outFd = fs3.openSync(logPath, "w");
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
- fs3.closeSync(outFd);
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 = fs3.readFileSync(logPath, "utf-8").slice(-2e3);
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 = path3.join(projectDir, ".env");
425
- if (!fs3.existsSync(dotenvPath)) return {};
705
+ const dotenvPath = path5.join(projectDir, ".env");
706
+ if (!fs6.existsSync(dotenvPath)) return {};
426
707
  const result = {};
427
- for (const rawLine of fs3.readFileSync(dotenvPath, "utf-8").split("\n")) {
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 fs4 from "fs";
451
- import * as path4 from "path";
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 (!fs4.existsSync(profilePath)) {
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(fs4.readFileSync(profilePath, "utf-8"));
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: path4.dirname(profilePath)
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 fs5 from "fs";
953
+ import * as fs8 from "fs";
673
954
  import * as os2 from "os";
674
- import * as path5 from "path";
955
+ import * as path7 from "path";
675
956
  function getPluginsCatalogRoot() {
676
- const here = path5.dirname(new URL(import.meta.url).pathname);
957
+ const here = path7.dirname(new URL(import.meta.url).pathname);
677
958
  const candidates = [
678
- path5.join(here, "..", "plugins"),
959
+ path7.join(here, "..", "plugins"),
679
960
  // dev: src/scripts → src/plugins
680
- path5.join(here, "..", "..", "plugins"),
961
+ path7.join(here, "..", "..", "plugins"),
681
962
  // built: dist/scripts → dist/plugins
682
- path5.join(here, "..", "..", "src", "plugins")
963
+ path7.join(here, "..", "..", "src", "plugins")
683
964
  // fallback
684
965
  ];
685
966
  for (const c of candidates) {
686
- if (fs5.existsSync(c) && fs5.statSync(c).isDirectory()) return c;
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 = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
697
- fs5.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
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 = path5.join(root, "skills");
700
- fs5.mkdirSync(dst, { recursive: true });
980
+ const dst = path7.join(root, "skills");
981
+ fs8.mkdirSync(dst, { recursive: true });
701
982
  for (const name of cc.skills) {
702
- const src = path5.join(catalog, "skills", name);
703
- if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
704
- copyDir(src, path5.join(dst, name));
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 = path5.join(root, "commands");
709
- fs5.mkdirSync(dst, { recursive: true });
989
+ const dst = path7.join(root, "commands");
990
+ fs8.mkdirSync(dst, { recursive: true });
710
991
  for (const name of cc.commands) {
711
- const src = path5.join(catalog, "commands", `${name}.md`);
712
- if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
713
- fs5.copyFileSync(src, path5.join(dst, `${name}.md`));
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 = path5.join(root, "agents");
718
- fs5.mkdirSync(dst, { recursive: true });
998
+ const dst = path7.join(root, "agents");
999
+ fs8.mkdirSync(dst, { recursive: true });
719
1000
  for (const name of cc.subagents) {
720
- const src = path5.join(catalog, "agents", `${name}.md`);
721
- if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
722
- fs5.copyFileSync(src, path5.join(dst, `${name}.md`));
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 = path5.join(root, "hooks");
727
- fs5.mkdirSync(dst, { recursive: true });
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 = path5.join(catalog, "hooks", `${name}.json`);
731
- if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
732
- const parsed = JSON.parse(fs5.readFileSync(src, "utf-8"));
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
- fs5.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
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
- fs5.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
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
- fs5.mkdirSync(dst, { recursive: true });
756
- for (const ent of fs5.readdirSync(src, { withFileTypes: true })) {
757
- const s = path5.join(src, ent.name);
758
- const d = path5.join(dst, ent.name);
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()) fs5.copyFileSync(s, d);
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 fs6 from "fs";
827
- import * as path6 from "path";
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 = path6.join(projectDir, rel);
834
- if (!fs6.existsSync(abs)) continue;
1114
+ const abs = path8.join(projectDir, rel);
1115
+ if (!fs9.existsSync(abs)) continue;
835
1116
  let content;
836
1117
  try {
837
- content = fs6.readFileSync(abs, "utf-8");
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) return { done: false, commitMessage: "", prSummary: "", failureReason: "agent produced no final message" };
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 { done: false, commitMessage: "", prSummary: "", failureReason: "no DONE or FAILED marker in agent output" };
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 fs7 from "fs";
915
- import * as path7 from "path";
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 = path7.join(cwd ?? process.cwd(), ".git");
971
- if (!fs7.existsSync(gitDir)) return aborted;
972
- if (fs7.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
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 (fs7.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
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 (fs7.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
1285
+ if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
979
1286
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
980
1287
  }
981
- if (fs7.existsSync(path7.join(gitDir, "rebase-merge")) || fs7.existsSync(path7.join(gitDir, "rebase-apply"))) {
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 = fs7.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
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 fs8 from "fs";
1114
- import * as path8 from "path";
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 ? path8.join(profile.dir, explicit) : null,
1121
- mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1122
- path8.join(profile.dir, "prompt.md")
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 (fs8.existsSync(c)) {
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 = fs8.readFileSync(templatePath, "utf-8");
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 fs9 from "fs";
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 || !fs9.existsSync(eventPath)) return;
1924
+ if (!eventPath || !fs12.existsSync(eventPath)) return;
1618
1925
  let event = null;
1619
1926
  try {
1620
- event = JSON.parse(fs9.readFileSync(eventPath, "utf-8"));
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 fs11 from "fs";
1847
- import * as path10 from "path";
2153
+ import * as fs14 from "fs";
2154
+ import * as path12 from "path";
1848
2155
 
1849
2156
  // src/registry.ts
1850
- import * as fs10 from "fs";
1851
- import * as path9 from "path";
2157
+ import * as fs13 from "fs";
2158
+ import * as path11 from "path";
1852
2159
  function getExecutablesRoot() {
1853
- const here = path9.dirname(new URL(import.meta.url).pathname);
2160
+ const here = path11.dirname(new URL(import.meta.url).pathname);
1854
2161
  const candidates = [
1855
- path9.join(here, "executables"),
2162
+ path11.join(here, "executables"),
1856
2163
  // dev: src/
1857
- path9.join(here, "..", "executables"),
2164
+ path11.join(here, "..", "executables"),
1858
2165
  // built: dist/bin → dist/executables
1859
- path9.join(here, "..", "src", "executables")
2166
+ path11.join(here, "..", "src", "executables")
1860
2167
  // fallback
1861
2168
  ];
1862
2169
  for (const c of candidates) {
1863
- if (fs10.existsSync(c) && fs10.statSync(c).isDirectory()) return c;
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 (!fs10.existsSync(root)) return [];
1869
- const entries = fs10.readdirSync(root, { withFileTypes: true });
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 = path9.join(root, ent.name, "profile.json");
1874
- if (fs10.existsSync(profilePath) && fs10.statSync(profilePath).isFile()) {
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 = path9.join(root, name, "profile.json");
1883
- return fs10.existsSync(profilePath) && fs10.statSync(profilePath).isFile();
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 (fs11.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1913
- if (fs11.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
1914
- if (fs11.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
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 = path10.join(cwd, "kody.config.json");
2036
- if (fs11.existsSync(configPath) && !force) {
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
- fs11.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2347
+ fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2041
2348
  `);
2042
2349
  wrote.push("kody.config.json");
2043
2350
  }
2044
- const workflowDir = path10.join(cwd, ".github", "workflows");
2045
- const workflowPath = path10.join(workflowDir, "kody2.yml");
2046
- if (fs11.existsSync(workflowPath) && !force) {
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
- fs11.mkdirSync(workflowDir, { recursive: true });
2050
- fs11.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
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 = path10.join(workflowDir, `kody2-${exe.name}.yml`);
2062
- if (fs11.existsSync(target) && !force) {
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
- fs11.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
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(JSON.stringify(
2281
- {
2282
- schemaVersion: state.schemaVersion,
2283
- core: state.core,
2284
- artifacts: state.artifacts ?? {},
2285
- executables: state.executables,
2286
- history: state.history
2287
- },
2288
- null,
2289
- 2
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 fs12 from "fs";
2556
- import * as path11 from "path";
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 = path11.join(cwd, file);
2573
- if (!fs12.existsSync(abs)) return false;
2574
- const content = fs12.readFileSync(abs, "utf-8");
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
- fs12.writeFileSync(abs, updated);
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 = path11.join(cwd, "CHANGELOG.md");
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 (fs12.existsSync(p)) {
2628
- const prior = fs12.readFileSync(p, "utf-8");
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
- fs12.writeFileSync(p, `${prior.slice(0, idx + 1)}
2939
+ fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
2632
2940
  ${entry}${prior.slice(idx + 1)}`);
2633
2941
  } else {
2634
- fs12.writeFileSync(p, `${header}${entry}${prior}`);
2942
+ fs15.writeFileSync(p, `${header}${entry}${prior}`);
2635
2943
  }
2636
2944
  } else {
2637
- fs12.writeFileSync(p, `${header}${entry}`);
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 = path11.join(cwd, "package.json");
2689
- if (!fs12.existsSync(pkgPath)) {
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(fs12.readFileSync(pkgPath, "utf-8"));
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
- ["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"],
2750
- { input: body, cwd }
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 = path11.join(cwd, "package.json");
2766
- const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
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(`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
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((resolve3) => {
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
- resolve3({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
3504
+ resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
3178
3505
  });
3179
3506
  child.on("error", (err) => {
3180
3507
  clearTimeout(timer);
3181
- resolve3({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
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(`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
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 fs13 from "fs";
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
- fs13.appendFileSync(summaryPath, `${lines.join("\n")}
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 = path12.join(input.cwd, ".kody2");
3792
+ const ndjsonDir = path14.join(input.cwd, ".kody2");
3475
3793
  const invokeAgent = async (prompt) => {
3476
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
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 = path12.dirname(new URL(import.meta.url).pathname);
3860
+ const here = path14.dirname(new URL(import.meta.url).pathname);
3543
3861
  const candidates = [
3544
- path12.join(here, "executables", profileName, "profile.json"),
3862
+ path14.join(here, "executables", profileName, "profile.json"),
3545
3863
  // same-dir sibling (dev)
3546
- path12.join(here, "..", "executables", profileName, "profile.json"),
3864
+ path14.join(here, "..", "executables", profileName, "profile.json"),
3547
3865
  // up one (prod: dist/bin → dist/executables)
3548
- path12.join(here, "..", "src", "executables", profileName, "profile.json")
3866
+ path14.join(here, "..", "src", "executables", profileName, "profile.json")
3549
3867
  // fallback
3550
3868
  ];
3551
3869
  for (const c of candidates) {
3552
- if (fs14.existsSync(c)) return c;
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 (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3818
- if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
3819
- if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
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 = path13.join(cwd, ".kody2", "last-run.jsonl");
4128
+ const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
3900
4129
  let tail = "";
3901
4130
  try {
3902
- if (fs16.existsSync(logPath)) {
3903
- const content = fs16.readFileSync(logPath, "utf-8");
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 ? path13.resolve(args.cwd) : process.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
  }