@kody-ade/kody-engine 0.2.32 → 0.2.34

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.
Files changed (2) hide show
  1. package/dist/bin/kody2.js +422 -375
  2. package/package.json +1 -1
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.32",
6
+ version: "0.2.34",
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",
@@ -50,81 +50,115 @@ var package_default = {
50
50
  };
51
51
 
52
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";
53
+ import * as path14 from "path";
56
54
 
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`);
55
+ // src/chat/pull.ts
56
+ var PullError = class extends Error {
57
+ constructor(message, status) {
58
+ super(message);
59
+ this.status = status;
60
+ this.name = "PullError";
61
+ }
62
+ status;
63
+ };
64
+ function createPullClient(opts) {
65
+ const fetchFn = opts.fetchFn ?? fetch;
66
+ const parsed = parseUrl(opts.baseUrl);
67
+ const token = opts.token ?? parsed.token;
68
+ if (!token) {
69
+ throw new PullError("session token not provided (expected inline ?token= in dashboardUrl)");
70
+ }
71
+ return async function pull(since, timeoutMs) {
72
+ const url = new URL(parsed.origin);
73
+ url.pathname = "/api/kody/chat/pull";
74
+ url.searchParams.set("sessionId", opts.sessionId);
75
+ url.searchParams.set("since", String(since));
76
+ url.searchParams.set("timeoutMs", String(timeoutMs));
77
+ url.searchParams.set("token", token);
78
+ const abort = AbortSignal.timeout(timeoutMs + 1e4);
79
+ const res = await fetchFn(url.toString(), {
80
+ method: "GET",
81
+ headers: { authorization: `Bearer ${token}` },
82
+ signal: abort
83
+ });
84
+ if (!res.ok) {
85
+ const body = await res.text().catch(() => "");
86
+ throw new PullError(`pull ${url.pathname} \u2192 ${res.status}: ${body.slice(0, 200)}`, res.status);
87
+ }
88
+ const data = await res.json();
89
+ return data;
90
+ };
62
91
  }
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
- `);
92
+ function parseUrl(baseUrl) {
93
+ try {
94
+ const u = new URL(baseUrl);
95
+ const token = u.searchParams.get("token");
96
+ const origin = `${u.protocol}//${u.host}${u.pathname !== "/" ? u.pathname : ""}`.replace(/\/$/, "");
97
+ return { origin: origin || u.origin, token };
98
+ } catch {
99
+ return { origin: baseUrl, token: null };
72
100
  }
73
- };
101
+ }
102
+
103
+ // src/chat/events.ts
74
104
  var HttpSink = class {
75
- constructor(baseUrl, sessionId, logger = {
105
+ constructor(baseUrl, sessionId, token, fetchFn = fetch, logger = {
76
106
  warn: (m) => process.stderr.write(`[kody2:chat] ${m}
77
107
  `)
78
108
  }) {
79
- this.baseUrl = baseUrl;
80
109
  this.sessionId = sessionId;
110
+ this.fetchFn = fetchFn;
81
111
  this.logger = logger;
112
+ const parsed = parseUrl(baseUrl);
113
+ this.origin = parsed.origin;
114
+ const resolved = token ?? parsed.token;
115
+ if (!resolved) {
116
+ throw new Error("HttpSink: session token not provided (expected inline ?token= in baseUrl)");
117
+ }
118
+ this.token = resolved;
82
119
  }
83
- baseUrl;
84
120
  sessionId;
121
+ fetchFn;
85
122
  logger;
123
+ origin;
124
+ token;
86
125
  async emit(event) {
87
- const url = withSessionParam(this.baseUrl, this.sessionId);
126
+ const url = new URL(this.origin);
127
+ url.pathname = "/api/kody/events/ingest";
128
+ url.searchParams.set("sessionId", this.sessionId);
129
+ url.searchParams.set("token", this.token);
88
130
  try {
89
- const res = await fetch(url, {
131
+ const res = await this.fetchFn(url.toString(), {
90
132
  method: "POST",
91
- headers: { "content-type": "application/json" },
133
+ headers: {
134
+ "content-type": "application/json",
135
+ authorization: `Bearer ${this.token}`
136
+ },
92
137
  body: JSON.stringify(event),
93
138
  signal: AbortSignal.timeout(5e3)
94
139
  });
95
140
  if (!res.ok) {
96
- this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
141
+ this.logger.warn(`HttpSink POST ${url.pathname} \u2192 ${res.status}`);
97
142
  }
98
143
  } catch (err) {
99
- this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
144
+ this.logger.warn(
145
+ `HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
146
+ );
100
147
  }
101
148
  }
102
149
  };
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
150
  function makeRunId(sessionId, suffix) {
117
151
  return `chat-${sessionId}-${suffix}`;
118
152
  }
119
153
 
120
154
  // src/agent.ts
121
- import * as fs3 from "fs";
122
- import * as path3 from "path";
155
+ import * as fs2 from "fs";
156
+ import * as path2 from "path";
123
157
  import { query } from "@anthropic-ai/claude-agent-sdk";
124
158
 
125
159
  // src/config.ts
126
- import * as fs2 from "fs";
127
- import * as path2 from "path";
160
+ import * as fs from "fs";
161
+ import * as path from "path";
128
162
  var LITELLM_DEFAULT_PORT = 4e3;
129
163
  var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
130
164
  function parseProviderModel(s) {
@@ -142,13 +176,13 @@ function needsLitellmProxy(model) {
142
176
  return model.provider !== "claude" && model.provider !== "anthropic";
143
177
  }
144
178
  function loadConfig(projectDir = process.cwd()) {
145
- const configPath = path2.join(projectDir, "kody.config.json");
146
- if (!fs2.existsSync(configPath)) {
179
+ const configPath = path.join(projectDir, "kody.config.json");
180
+ if (!fs.existsSync(configPath)) {
147
181
  throw new Error(`kody.config.json not found at ${configPath}`);
148
182
  }
149
183
  let raw;
150
184
  try {
151
- raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
185
+ raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
152
186
  } catch (err) {
153
187
  const msg = err instanceof Error ? err.message : String(err);
154
188
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
@@ -325,10 +359,10 @@ function formatBytes(bytes) {
325
359
  // src/agent.ts
326
360
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
327
361
  async function runAgent(opts) {
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" });
362
+ const ndjsonDir = opts.ndjsonDir ?? path2.join(opts.cwd, ".kody2");
363
+ fs2.mkdirSync(ndjsonDir, { recursive: true });
364
+ const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
365
+ const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
332
366
  const env = {
333
367
  ...process.env,
334
368
  SKIP_HOOKS: "1",
@@ -400,53 +434,6 @@ async function runAgent(opts) {
400
434
  return { outcome, finalText, error: errorMessage, ndjsonPath };
401
435
  }
402
436
 
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
437
  // src/chat/loop.ts
451
438
  var CHAT_SYSTEM_PROMPT = [
452
439
  "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
@@ -455,20 +442,15 @@ var CHAT_SYSTEM_PROMPT = [
455
442
  "read repository code or execute small checks when it helps you answer \u2014 otherwise",
456
443
  "reply directly. Do not invent file paths, commit SHAs, or command output."
457
444
  ].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);
445
+ var DEFAULT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
446
+ var DEFAULT_HARD_TIMEOUT_MS = 5 * 60 * 60 * 1e3;
447
+ var DEFAULT_PULL_TIMEOUT_MS = 25e3;
448
+ async function runChatSession(opts) {
449
+ const now = opts.now ?? (() => Date.now());
450
+ const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
451
+ const hardTimeoutMs = opts.hardTimeoutMs ?? DEFAULT_HARD_TIMEOUT_MS;
452
+ const pullTimeoutMs = opts.pullTimeoutMs ?? DEFAULT_PULL_TIMEOUT_MS;
453
+ const systemPrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
472
454
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
473
455
  prompt: p,
474
456
  model: opts.model,
@@ -477,34 +459,68 @@ async function runChatTurn(opts) {
477
459
  verbose: opts.verbose,
478
460
  quiet: opts.quiet
479
461
  }));
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 };
462
+ const history = [];
463
+ let since = 0;
464
+ let lastActivityAt = now();
465
+ const startedAt = now();
466
+ let turnsProcessed = 0;
467
+ while (true) {
468
+ if (now() - startedAt > hardTimeoutMs) {
469
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
470
+ sessionId: opts.sessionId,
471
+ reason: "hard-timeout"
472
+ });
473
+ return { exitCode: 0, turnsProcessed, reason: "hard-timeout" };
474
+ }
475
+ let response;
476
+ try {
477
+ response = await opts.pull(since, pullTimeoutMs);
478
+ } catch (err) {
479
+ const msg = err instanceof Error ? err.message : String(err);
480
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
481
+ return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
482
+ }
483
+ if (response.turns.length === 0) {
484
+ if (now() - lastActivityAt > idleTimeoutMs) {
485
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
486
+ sessionId: opts.sessionId,
487
+ reason: "idle-timeout"
488
+ });
489
+ return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
490
+ }
491
+ continue;
492
+ }
493
+ const newUserTurns = response.turns.filter((t) => t.role === "user");
494
+ for (const t of newUserTurns) history.push(t);
495
+ since = response.nextSince;
496
+ if (newUserTurns.length === 0) continue;
497
+ lastActivityAt = now();
498
+ const prompt = buildPrompt(history, systemPrompt);
499
+ let result;
500
+ try {
501
+ result = await invoke(prompt);
502
+ } catch (err) {
503
+ const msg = err instanceof Error ? err.message : String(err);
504
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
505
+ return { exitCode: 99, turnsProcessed, reason: msg };
506
+ }
507
+ if (result.outcome !== "completed") {
508
+ const error = result.error ?? "agent did not complete";
509
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
510
+ return { exitCode: 99, turnsProcessed, reason: error };
511
+ }
512
+ const reply = result.finalText.trim();
513
+ const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
514
+ history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
515
+ turnsProcessed++;
516
+ lastActivityAt = now();
517
+ await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
518
+ sessionId: opts.sessionId,
519
+ role: "assistant",
520
+ content: reply,
521
+ timestamp: replyTimestamp
522
+ });
492
523
  }
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
524
  }
509
525
  function buildPrompt(turns, systemPrompt) {
510
526
  const header = `System: ${systemPrompt}`;
@@ -526,11 +542,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
526
542
 
527
543
  // src/kody2-cli.ts
528
544
  import { execFileSync as execFileSync15 } from "child_process";
529
- import * as fs18 from "fs";
530
- import * as path15 from "path";
545
+ import * as fs16 from "fs";
546
+ import * as path13 from "path";
531
547
 
532
548
  // src/dispatch.ts
533
- import * as fs5 from "fs";
549
+ import * as fs3 from "fs";
534
550
  function autoDispatch(opts) {
535
551
  const explicit = opts?.explicit;
536
552
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
@@ -542,10 +558,10 @@ function autoDispatch(opts) {
542
558
  }
543
559
  const eventName = process.env.GITHUB_EVENT_NAME;
544
560
  const eventPath = process.env.GITHUB_EVENT_PATH;
545
- if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
561
+ if (!eventName || !eventPath || !fs3.existsSync(eventPath)) return null;
546
562
  let event = {};
547
563
  try {
548
- event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
564
+ event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
549
565
  } catch {
550
566
  return null;
551
567
  }
@@ -614,14 +630,14 @@ function extractFeedback(afterTag) {
614
630
  }
615
631
 
616
632
  // src/executor.ts
617
- import * as fs17 from "fs";
618
- import * as path14 from "path";
633
+ import * as fs15 from "fs";
634
+ import * as path12 from "path";
619
635
 
620
636
  // src/litellm.ts
621
637
  import { execFileSync, spawn } from "child_process";
622
- import * as fs6 from "fs";
638
+ import * as fs4 from "fs";
623
639
  import * as os from "os";
624
- import * as path5 from "path";
640
+ import * as path3 from "path";
625
641
  async function checkLitellmHealth(url) {
626
642
  try {
627
643
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -661,20 +677,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
661
677
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
662
678
  }
663
679
  }
664
- const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
665
- fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
680
+ const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
681
+ fs4.writeFileSync(configPath, generateLitellmConfigYaml(model));
666
682
  const portMatch = url.match(/:(\d+)/);
667
683
  const port = portMatch ? portMatch[1] : "4000";
668
684
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
669
685
  const dotenvVars = readDotenvApiKeys(projectDir);
670
- const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
671
- const outFd = fs6.openSync(logPath, "w");
686
+ const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
687
+ const outFd = fs4.openSync(logPath, "w");
672
688
  const child = spawn(cmd, args, {
673
689
  stdio: ["ignore", outFd, outFd],
674
690
  detached: true,
675
691
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
676
692
  });
677
- fs6.closeSync(outFd);
693
+ fs4.closeSync(outFd);
678
694
  for (let i = 0; i < 30; i++) {
679
695
  await new Promise((r) => setTimeout(r, 2e3));
680
696
  if (await checkLitellmHealth(url)) {
@@ -691,7 +707,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
691
707
  }
692
708
  let logTail = "";
693
709
  try {
694
- logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
710
+ logTail = fs4.readFileSync(logPath, "utf-8").slice(-2e3);
695
711
  } catch {
696
712
  }
697
713
  try {
@@ -702,10 +718,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
702
718
  ${logTail}`);
703
719
  }
704
720
  function readDotenvApiKeys(projectDir) {
705
- const dotenvPath = path5.join(projectDir, ".env");
706
- if (!fs6.existsSync(dotenvPath)) return {};
721
+ const dotenvPath = path3.join(projectDir, ".env");
722
+ if (!fs4.existsSync(dotenvPath)) return {};
707
723
  const result = {};
708
- for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
724
+ for (const rawLine of fs4.readFileSync(dotenvPath, "utf-8").split("\n")) {
709
725
  const line = rawLine.trim();
710
726
  if (!line || line.startsWith("#")) continue;
711
727
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -728,8 +744,8 @@ function stripBlockingEnv(env) {
728
744
  }
729
745
 
730
746
  // src/profile.ts
731
- import * as fs7 from "fs";
732
- import * as path6 from "path";
747
+ import * as fs5 from "fs";
748
+ import * as path4 from "path";
733
749
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
734
750
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
735
751
  var ProfileError = class extends Error {
@@ -742,12 +758,12 @@ var ProfileError = class extends Error {
742
758
  profilePath;
743
759
  };
744
760
  function loadProfile(profilePath) {
745
- if (!fs7.existsSync(profilePath)) {
761
+ if (!fs5.existsSync(profilePath)) {
746
762
  throw new ProfileError(profilePath, "file not found");
747
763
  }
748
764
  let raw;
749
765
  try {
750
- raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
766
+ raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
751
767
  } catch (err) {
752
768
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
753
769
  }
@@ -771,7 +787,7 @@ function loadProfile(profilePath) {
771
787
  outputContract: r.outputContract,
772
788
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
773
789
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
774
- dir: path6.dirname(profilePath)
790
+ dir: path4.dirname(profilePath)
775
791
  };
776
792
  return profile;
777
793
  }
@@ -950,21 +966,21 @@ function parseScriptList(p, key, raw) {
950
966
  }
951
967
 
952
968
  // src/scripts/buildSyntheticPlugin.ts
953
- import * as fs8 from "fs";
969
+ import * as fs6 from "fs";
954
970
  import * as os2 from "os";
955
- import * as path7 from "path";
971
+ import * as path5 from "path";
956
972
  function getPluginsCatalogRoot() {
957
- const here = path7.dirname(new URL(import.meta.url).pathname);
973
+ const here = path5.dirname(new URL(import.meta.url).pathname);
958
974
  const candidates = [
959
- path7.join(here, "..", "plugins"),
975
+ path5.join(here, "..", "plugins"),
960
976
  // dev: src/scripts → src/plugins
961
- path7.join(here, "..", "..", "plugins"),
977
+ path5.join(here, "..", "..", "plugins"),
962
978
  // built: dist/scripts → dist/plugins
963
- path7.join(here, "..", "..", "src", "plugins")
979
+ path5.join(here, "..", "..", "src", "plugins")
964
980
  // fallback
965
981
  ];
966
982
  for (const c of candidates) {
967
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
983
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
968
984
  }
969
985
  return candidates[0];
970
986
  }
@@ -974,50 +990,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
974
990
  if (!needsSynthetic) return;
975
991
  const catalog = getPluginsCatalogRoot();
976
992
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
977
- const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
978
- fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
993
+ const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
994
+ fs6.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
979
995
  if (cc.skills.length > 0) {
980
- const dst = path7.join(root, "skills");
981
- fs8.mkdirSync(dst, { recursive: true });
996
+ const dst = path5.join(root, "skills");
997
+ fs6.mkdirSync(dst, { recursive: true });
982
998
  for (const name of cc.skills) {
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));
999
+ const src = path5.join(catalog, "skills", name);
1000
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1001
+ copyDir(src, path5.join(dst, name));
986
1002
  }
987
1003
  }
988
1004
  if (cc.commands.length > 0) {
989
- const dst = path7.join(root, "commands");
990
- fs8.mkdirSync(dst, { recursive: true });
1005
+ const dst = path5.join(root, "commands");
1006
+ fs6.mkdirSync(dst, { recursive: true });
991
1007
  for (const name of cc.commands) {
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`));
1008
+ const src = path5.join(catalog, "commands", `${name}.md`);
1009
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1010
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
995
1011
  }
996
1012
  }
997
1013
  if (cc.subagents.length > 0) {
998
- const dst = path7.join(root, "agents");
999
- fs8.mkdirSync(dst, { recursive: true });
1014
+ const dst = path5.join(root, "agents");
1015
+ fs6.mkdirSync(dst, { recursive: true });
1000
1016
  for (const name of cc.subagents) {
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`));
1017
+ const src = path5.join(catalog, "agents", `${name}.md`);
1018
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1019
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
1004
1020
  }
1005
1021
  }
1006
1022
  if (cc.hooks.length > 0) {
1007
- const dst = path7.join(root, "hooks");
1008
- fs8.mkdirSync(dst, { recursive: true });
1023
+ const dst = path5.join(root, "hooks");
1024
+ fs6.mkdirSync(dst, { recursive: true });
1009
1025
  const merged = { hooks: {} };
1010
1026
  for (const name of cc.hooks) {
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"));
1027
+ const src = path5.join(catalog, "hooks", `${name}.json`);
1028
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1029
+ const parsed = JSON.parse(fs6.readFileSync(src, "utf-8"));
1014
1030
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1015
1031
  if (!Array.isArray(entries)) continue;
1016
1032
  if (!merged.hooks[event]) merged.hooks[event] = [];
1017
1033
  merged.hooks[event].push(...entries);
1018
1034
  }
1019
1035
  }
1020
- fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1036
+ fs6.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1021
1037
  `);
1022
1038
  }
1023
1039
  const manifest = {
@@ -1028,17 +1044,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1028
1044
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1029
1045
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1030
1046
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1031
- fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1047
+ fs6.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1032
1048
  `);
1033
1049
  ctx.data.syntheticPluginPath = root;
1034
1050
  };
1035
1051
  function copyDir(src, dst) {
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);
1052
+ fs6.mkdirSync(dst, { recursive: true });
1053
+ for (const ent of fs6.readdirSync(src, { withFileTypes: true })) {
1054
+ const s = path5.join(src, ent.name);
1055
+ const d = path5.join(dst, ent.name);
1040
1056
  if (ent.isDirectory()) copyDir(s, d);
1041
- else if (ent.isFile()) fs8.copyFileSync(s, d);
1057
+ else if (ent.isFile()) fs6.copyFileSync(s, d);
1042
1058
  }
1043
1059
  }
1044
1060
 
@@ -1104,18 +1120,18 @@ function formatMissesForFeedback(misses) {
1104
1120
  }
1105
1121
 
1106
1122
  // src/prompt.ts
1107
- import * as fs9 from "fs";
1108
- import * as path8 from "path";
1123
+ import * as fs7 from "fs";
1124
+ import * as path6 from "path";
1109
1125
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1110
1126
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1111
1127
  function loadProjectConventions(projectDir) {
1112
1128
  const out = [];
1113
1129
  for (const rel of CONVENTION_FILES) {
1114
- const abs = path8.join(projectDir, rel);
1115
- if (!fs9.existsSync(abs)) continue;
1130
+ const abs = path6.join(projectDir, rel);
1131
+ if (!fs7.existsSync(abs)) continue;
1116
1132
  let content;
1117
1133
  try {
1118
- content = fs9.readFileSync(abs, "utf-8");
1134
+ content = fs7.readFileSync(abs, "utf-8");
1119
1135
  } catch {
1120
1136
  continue;
1121
1137
  }
@@ -1236,8 +1252,8 @@ import { execFileSync as execFileSync4 } from "child_process";
1236
1252
 
1237
1253
  // src/commit.ts
1238
1254
  import { execFileSync as execFileSync3 } from "child_process";
1239
- import * as fs10 from "fs";
1240
- import * as path9 from "path";
1255
+ import * as fs8 from "fs";
1256
+ import * as path7 from "path";
1241
1257
  var FORBIDDEN_PATH_PREFIXES = [
1242
1258
  ".kody/",
1243
1259
  ".kody-engine/",
@@ -1292,18 +1308,18 @@ function tryGit(args, cwd) {
1292
1308
  }
1293
1309
  function abortUnfinishedGitOps(cwd) {
1294
1310
  const aborted = [];
1295
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1296
- if (!fs10.existsSync(gitDir)) return aborted;
1297
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1311
+ const gitDir = path7.join(cwd ?? process.cwd(), ".git");
1312
+ if (!fs8.existsSync(gitDir)) return aborted;
1313
+ if (fs8.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
1298
1314
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1299
1315
  }
1300
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1316
+ if (fs8.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
1301
1317
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1302
1318
  }
1303
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1319
+ if (fs8.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
1304
1320
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1305
1321
  }
1306
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1322
+ if (fs8.existsSync(path7.join(gitDir, "rebase-merge")) || fs8.existsSync(path7.join(gitDir, "rebase-apply"))) {
1307
1323
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1308
1324
  }
1309
1325
  try {
@@ -1345,7 +1361,7 @@ function normalizeCommitMessage(raw) {
1345
1361
  function commitAndPush(branch, agentMessage, cwd) {
1346
1362
  const allChanged = listChangedFiles(cwd);
1347
1363
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1348
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1364
+ const mergeHeadExists = fs8.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1349
1365
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1350
1366
  return { committed: false, pushed: false, sha: "", message: "" };
1351
1367
  }
@@ -1440,20 +1456,20 @@ function defaultCommitMessage(mode, data) {
1440
1456
  }
1441
1457
 
1442
1458
  // src/scripts/composePrompt.ts
1443
- import * as fs11 from "fs";
1444
- import * as path10 from "path";
1459
+ import * as fs9 from "fs";
1460
+ import * as path8 from "path";
1445
1461
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1446
1462
  var composePrompt = async (ctx, profile) => {
1447
1463
  const explicit = ctx.data.promptTemplate;
1448
1464
  const mode = ctx.args.mode;
1449
1465
  const candidates = [
1450
- explicit ? path10.join(profile.dir, explicit) : null,
1451
- mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1452
- path10.join(profile.dir, "prompt.md")
1466
+ explicit ? path8.join(profile.dir, explicit) : null,
1467
+ mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1468
+ path8.join(profile.dir, "prompt.md")
1453
1469
  ].filter(Boolean);
1454
1470
  let templatePath = "";
1455
1471
  for (const c of candidates) {
1456
- if (fs11.existsSync(c)) {
1472
+ if (fs9.existsSync(c)) {
1457
1473
  templatePath = c;
1458
1474
  break;
1459
1475
  }
@@ -1461,7 +1477,7 @@ var composePrompt = async (ctx, profile) => {
1461
1477
  if (!templatePath) {
1462
1478
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1463
1479
  }
1464
- const template = fs11.readFileSync(templatePath, "utf-8");
1480
+ const template = fs9.readFileSync(templatePath, "utf-8");
1465
1481
  const tokens = {
1466
1482
  ...stringifyAll(ctx.args, "args."),
1467
1483
  ...stringifyAll(ctx.data, ""),
@@ -1936,7 +1952,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1936
1952
 
1937
1953
  // src/gha.ts
1938
1954
  import { execFileSync as execFileSync7 } from "child_process";
1939
- import * as fs12 from "fs";
1955
+ import * as fs10 from "fs";
1940
1956
  function getRunUrl() {
1941
1957
  const server = process.env.GITHUB_SERVER_URL;
1942
1958
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1947,10 +1963,10 @@ function getRunUrl() {
1947
1963
  function reactToTriggerComment(cwd) {
1948
1964
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1949
1965
  const eventPath = process.env.GITHUB_EVENT_PATH;
1950
- if (!eventPath || !fs12.existsSync(eventPath)) return;
1966
+ if (!eventPath || !fs10.existsSync(eventPath)) return;
1951
1967
  let event = null;
1952
1968
  try {
1953
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
1969
+ event = JSON.parse(fs10.readFileSync(eventPath, "utf-8"));
1954
1970
  } catch {
1955
1971
  return;
1956
1972
  }
@@ -2176,35 +2192,35 @@ function tryPostPr2(prNumber, body, cwd) {
2176
2192
 
2177
2193
  // src/scripts/initFlow.ts
2178
2194
  import { execFileSync as execFileSync9 } from "child_process";
2179
- import * as fs14 from "fs";
2180
- import * as path12 from "path";
2195
+ import * as fs12 from "fs";
2196
+ import * as path10 from "path";
2181
2197
 
2182
2198
  // src/registry.ts
2183
- import * as fs13 from "fs";
2184
- import * as path11 from "path";
2199
+ import * as fs11 from "fs";
2200
+ import * as path9 from "path";
2185
2201
  function getExecutablesRoot() {
2186
- const here = path11.dirname(new URL(import.meta.url).pathname);
2202
+ const here = path9.dirname(new URL(import.meta.url).pathname);
2187
2203
  const candidates = [
2188
- path11.join(here, "executables"),
2204
+ path9.join(here, "executables"),
2189
2205
  // dev: src/
2190
- path11.join(here, "..", "executables"),
2206
+ path9.join(here, "..", "executables"),
2191
2207
  // built: dist/bin → dist/executables
2192
- path11.join(here, "..", "src", "executables")
2208
+ path9.join(here, "..", "src", "executables")
2193
2209
  // fallback
2194
2210
  ];
2195
2211
  for (const c of candidates) {
2196
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2212
+ if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2197
2213
  }
2198
2214
  return candidates[0];
2199
2215
  }
2200
2216
  function listExecutables(root = getExecutablesRoot()) {
2201
- if (!fs13.existsSync(root)) return [];
2202
- const entries = fs13.readdirSync(root, { withFileTypes: true });
2217
+ if (!fs11.existsSync(root)) return [];
2218
+ const entries = fs11.readdirSync(root, { withFileTypes: true });
2203
2219
  const out = [];
2204
2220
  for (const ent of entries) {
2205
2221
  if (!ent.isDirectory()) continue;
2206
- const profilePath = path11.join(root, ent.name, "profile.json");
2207
- if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
2222
+ const profilePath = path9.join(root, ent.name, "profile.json");
2223
+ if (fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile()) {
2208
2224
  out.push({ name: ent.name, profilePath });
2209
2225
  }
2210
2226
  }
@@ -2212,8 +2228,8 @@ function listExecutables(root = getExecutablesRoot()) {
2212
2228
  }
2213
2229
  function hasExecutable(name, root = getExecutablesRoot()) {
2214
2230
  if (!isSafeName(name)) return false;
2215
- const profilePath = path11.join(root, name, "profile.json");
2216
- return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
2231
+ const profilePath = path9.join(root, name, "profile.json");
2232
+ return fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile();
2217
2233
  }
2218
2234
  function isSafeName(name) {
2219
2235
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -2242,9 +2258,9 @@ function parseGenericFlags(argv) {
2242
2258
 
2243
2259
  // src/scripts/initFlow.ts
2244
2260
  function detectPackageManager(cwd) {
2245
- if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2246
- if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
2247
- if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
2261
+ if (fs12.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2262
+ if (fs12.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2263
+ if (fs12.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2248
2264
  return "npm";
2249
2265
  }
2250
2266
  function qualityCommandsFor(pm) {
@@ -2365,22 +2381,22 @@ function performInit(cwd, force) {
2365
2381
  const pm = detectPackageManager(cwd);
2366
2382
  const ownerRepo = detectOwnerRepo(cwd);
2367
2383
  const defaultBranch = defaultBranchFromGit(cwd);
2368
- const configPath = path12.join(cwd, "kody.config.json");
2369
- if (fs14.existsSync(configPath) && !force) {
2384
+ const configPath = path10.join(cwd, "kody.config.json");
2385
+ if (fs12.existsSync(configPath) && !force) {
2370
2386
  skipped.push("kody.config.json");
2371
2387
  } else {
2372
2388
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
2373
- fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2389
+ fs12.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2374
2390
  `);
2375
2391
  wrote.push("kody.config.json");
2376
2392
  }
2377
- const workflowDir = path12.join(cwd, ".github", "workflows");
2378
- const workflowPath = path12.join(workflowDir, "kody2.yml");
2379
- if (fs14.existsSync(workflowPath) && !force) {
2393
+ const workflowDir = path10.join(cwd, ".github", "workflows");
2394
+ const workflowPath = path10.join(workflowDir, "kody2.yml");
2395
+ if (fs12.existsSync(workflowPath) && !force) {
2380
2396
  skipped.push(".github/workflows/kody2.yml");
2381
2397
  } else {
2382
- fs14.mkdirSync(workflowDir, { recursive: true });
2383
- fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2398
+ fs12.mkdirSync(workflowDir, { recursive: true });
2399
+ fs12.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2384
2400
  wrote.push(".github/workflows/kody2.yml");
2385
2401
  }
2386
2402
  for (const exe of listExecutables()) {
@@ -2391,12 +2407,12 @@ function performInit(cwd, force) {
2391
2407
  continue;
2392
2408
  }
2393
2409
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
2394
- const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
2395
- if (fs14.existsSync(target) && !force) {
2410
+ const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
2411
+ if (fs12.existsSync(target) && !force) {
2396
2412
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
2397
2413
  continue;
2398
2414
  }
2399
- fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2415
+ fs12.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2400
2416
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
2401
2417
  }
2402
2418
  return { wrote, skipped };
@@ -2903,8 +2919,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2903
2919
 
2904
2920
  // src/scripts/releaseFlow.ts
2905
2921
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2906
- import * as fs15 from "fs";
2907
- import * as path13 from "path";
2922
+ import * as fs13 from "fs";
2923
+ import * as path11 from "path";
2908
2924
  function bumpVersion(current, bump) {
2909
2925
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2910
2926
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2920,12 +2936,12 @@ function bumpVersion(current, bump) {
2920
2936
  return `${major}.${minor}.${patch}`;
2921
2937
  }
2922
2938
  function updateVersionInFile(file, newVersion, cwd) {
2923
- const abs = path13.join(cwd, file);
2924
- if (!fs15.existsSync(abs)) return false;
2925
- const content = fs15.readFileSync(abs, "utf-8");
2939
+ const abs = path11.join(cwd, file);
2940
+ if (!fs13.existsSync(abs)) return false;
2941
+ const content = fs13.readFileSync(abs, "utf-8");
2926
2942
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2927
2943
  if (updated === content) return false;
2928
- fs15.writeFileSync(abs, updated);
2944
+ fs13.writeFileSync(abs, updated);
2929
2945
  return true;
2930
2946
  }
2931
2947
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2973,19 +2989,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2973
2989
  return parts.join("\n");
2974
2990
  }
2975
2991
  function prependChangelog(cwd, entry) {
2976
- const p = path13.join(cwd, "CHANGELOG.md");
2992
+ const p = path11.join(cwd, "CHANGELOG.md");
2977
2993
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2978
- if (fs15.existsSync(p)) {
2979
- const prior = fs15.readFileSync(p, "utf-8");
2994
+ if (fs13.existsSync(p)) {
2995
+ const prior = fs13.readFileSync(p, "utf-8");
2980
2996
  if (/^#\s*Changelog\b/m.test(prior)) {
2981
2997
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2982
- fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
2998
+ fs13.writeFileSync(p, `${prior.slice(0, idx + 1)}
2983
2999
  ${entry}${prior.slice(idx + 1)}`);
2984
3000
  } else {
2985
- fs15.writeFileSync(p, `${header}${entry}${prior}`);
3001
+ fs13.writeFileSync(p, `${header}${entry}${prior}`);
2986
3002
  }
2987
3003
  } else {
2988
- fs15.writeFileSync(p, `${header}${entry}`);
3004
+ fs13.writeFileSync(p, `${header}${entry}`);
2989
3005
  }
2990
3006
  }
2991
3007
  function git3(args, cwd, timeout = 6e4) {
@@ -3036,13 +3052,13 @@ var releaseFlow = async (ctx) => {
3036
3052
  };
3037
3053
  async function runPrepare(args) {
3038
3054
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
3039
- const pkgPath = path13.join(cwd, "package.json");
3040
- if (!fs15.existsSync(pkgPath)) {
3055
+ const pkgPath = path11.join(cwd, "package.json");
3056
+ if (!fs13.existsSync(pkgPath)) {
3041
3057
  ctx.output.exitCode = 99;
3042
3058
  ctx.output.reason = "release prepare: package.json not found";
3043
3059
  return;
3044
3060
  }
3045
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3061
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3046
3062
  if (typeof pkg.version !== "string") {
3047
3063
  ctx.output.exitCode = 99;
3048
3064
  ctx.output.reason = "release prepare: package.json has no version";
@@ -3113,8 +3129,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
3113
3129
  }
3114
3130
  async function runFinalize(args) {
3115
3131
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
3116
- const pkgPath = path13.join(cwd, "package.json");
3117
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3132
+ const pkgPath = path11.join(cwd, "package.json");
3133
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3118
3134
  if (typeof pkg.version !== "string") {
3119
3135
  ctx.output.exitCode = 99;
3120
3136
  ctx.output.reason = "release finalize: package.json has no version";
@@ -3199,31 +3215,17 @@ ${truncate2(r.stderr, 2e3)}
3199
3215
 
3200
3216
  // src/scripts/requireFeedbackActions.ts
3201
3217
  var MIN_ITEMS = 1;
3202
- var ACTIONABLE_HEADINGS = /^#{1,6}\s+(Concerns|Suggestions|Bugs)\b/i;
3203
- var NEXT_HEADING = /^#{1,6}\s+/;
3204
3218
  var requireFeedbackActions = async (ctx, profile) => {
3205
3219
  if (!ctx.data.agentDone) return;
3206
3220
  const actions = String(ctx.data.feedbackActions ?? "").trim();
3207
3221
  const items = countActionItems(actions);
3222
+ ctx.data.feedbackAgentItemCount = items;
3208
3223
  if (items < MIN_ITEMS) {
3209
3224
  fail(
3210
3225
  ctx,
3211
3226
  profile,
3212
3227
  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"
3213
3228
  );
3214
- return;
3215
- }
3216
- const reviewBody = String(ctx.data.feedback ?? "");
3217
- const expectedItems = countActionableReviewBullets(reviewBody);
3218
- ctx.data.feedbackReviewItemCount = expectedItems;
3219
- ctx.data.feedbackAgentItemCount = items;
3220
- if (expectedItems > 0 && items < expectedItems) {
3221
- fail(
3222
- ctx,
3223
- profile,
3224
- `agent FEEDBACK_ACTIONS listed ${items} item(s) but the review has ${expectedItems} actionable bullet(s) under ### Concerns / ### Suggestions / ### Bugs \u2014 every review item must be accounted for`
3225
- );
3226
- return;
3227
3229
  }
3228
3230
  };
3229
3231
  function fail(ctx, profile, reason) {
@@ -3245,25 +3247,6 @@ function countActionItems(block) {
3245
3247
  }
3246
3248
  return count;
3247
3249
  }
3248
- function countActionableReviewBullets(reviewBody) {
3249
- if (!reviewBody.trim()) return 0;
3250
- const lines = reviewBody.split("\n");
3251
- let count = 0;
3252
- let insideActionable = false;
3253
- for (const raw of lines) {
3254
- if (ACTIONABLE_HEADINGS.test(raw)) {
3255
- insideActionable = true;
3256
- continue;
3257
- }
3258
- if (insideActionable && NEXT_HEADING.test(raw)) {
3259
- insideActionable = false;
3260
- continue;
3261
- }
3262
- if (!insideActionable) continue;
3263
- if (/^[-*]\s+\S/.test(raw)) count++;
3264
- }
3265
- return count;
3266
- }
3267
3250
 
3268
3251
  // src/scripts/requirePlanDeviations.ts
3269
3252
  var requirePlanDeviations = async (ctx, profile) => {
@@ -3683,6 +3666,44 @@ function summarizeFeedbackActions(block) {
3683
3666
  }
3684
3667
  return summary;
3685
3668
  }
3669
+ function extractReviewFileRefs(reviewBody) {
3670
+ if (!reviewBody) return [];
3671
+ const found = /* @__PURE__ */ new Set();
3672
+ const backtick = /`([^`\s]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?`/g;
3673
+ let m;
3674
+ while ((m = backtick.exec(reviewBody)) !== null) {
3675
+ const raw = m[1];
3676
+ if (isPlausibleSourcePath(raw)) found.add(raw);
3677
+ }
3678
+ const bare = /(?<![A-Za-z0-9/_.-])((?:[A-Za-z0-9_./-]+\/)+[A-Za-z0-9_.-]+\.[a-zA-Z]{1,5})(?::\d+(?:-\d+)?)?/g;
3679
+ while ((m = bare.exec(reviewBody)) !== null) {
3680
+ const raw = m[1];
3681
+ if (isPlausibleSourcePath(raw)) found.add(raw);
3682
+ }
3683
+ return Array.from(found);
3684
+ }
3685
+ function isPlausibleSourcePath(p) {
3686
+ if (p.startsWith("http://") || p.startsWith("https://")) return false;
3687
+ if (p.startsWith("//")) return false;
3688
+ if (p.startsWith("/")) return false;
3689
+ if (!p.includes("/")) return false;
3690
+ if (/\.(md|rst|txt|png|jpg|jpeg|gif|svg|pdf)$/i.test(p)) return false;
3691
+ const firstSeg = p.slice(0, p.indexOf("/"));
3692
+ if (firstSeg.includes(".")) return false;
3693
+ return true;
3694
+ }
3695
+ function declinedFileRefs(feedbackActions, refs) {
3696
+ if (!feedbackActions.trim() || refs.length === 0) return /* @__PURE__ */ new Set();
3697
+ const declined = /* @__PURE__ */ new Set();
3698
+ for (const raw of feedbackActions.split("\n")) {
3699
+ if (!/^\s*[-*]\s+/.test(raw)) continue;
3700
+ if (!/\bdeclined\s*:/i.test(raw)) continue;
3701
+ for (const ref of refs) {
3702
+ if (raw.includes(ref)) declined.add(ref);
3703
+ }
3704
+ }
3705
+ return declined;
3706
+ }
3686
3707
  function makeAction2(type, payload) {
3687
3708
  return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3688
3709
  }
@@ -3695,24 +3716,32 @@ var verifyFixAlignment = async (ctx, profile) => {
3695
3716
  ctx.data.feedbackActionsSummary = summary;
3696
3717
  const committed = Boolean(ctx.data.commitResult?.committed);
3697
3718
  if (summary.totalItems === 0) {
3698
- ctx.output.exitCode = 1;
3699
- ctx.output.reason = "fix produced no FEEDBACK_ACTIONS items";
3700
- ctx.data.agentDone = false;
3701
- ctx.data.action = makeAction2("FIX_FAILED", {
3702
- reason: ctx.output.reason,
3703
- feedbackActionsSummary: summary
3704
- });
3705
- return;
3719
+ return failOnce(ctx, "FIX_FAILED", "fix produced no FEEDBACK_ACTIONS items", summary);
3706
3720
  }
3707
3721
  if (summary.fixedItems > 0 && !committed) {
3708
- ctx.output.exitCode = 1;
3709
- ctx.output.reason = `fix claimed ${summary.fixedItems} fixed item(s) but produced no commit`;
3710
- ctx.data.agentDone = false;
3711
- ctx.data.action = makeAction2("FIX_FAILED", {
3712
- reason: ctx.output.reason,
3713
- feedbackActionsSummary: summary
3714
- });
3715
- return;
3722
+ return failOnce(
3723
+ ctx,
3724
+ "FIX_FAILED",
3725
+ `fix claimed ${summary.fixedItems} fixed item(s) but produced no commit`,
3726
+ summary
3727
+ );
3728
+ }
3729
+ const reviewBody = ctx.data.feedback ?? "";
3730
+ const refs = extractReviewFileRefs(reviewBody);
3731
+ const changedFiles = (ctx.data.changedFiles ?? []).map((f) => f.trim()).filter(Boolean);
3732
+ ctx.data.reviewFileRefs = refs;
3733
+ if (refs.length > 0 && committed) {
3734
+ const declined = declinedFileRefs(feedbackActions, refs);
3735
+ const missing = refs.filter((r) => !declined.has(r) && !changedFiles.some((f) => filesMatch(f, r)));
3736
+ if (missing.length > 0) {
3737
+ return failOnce(
3738
+ ctx,
3739
+ "FIX_FAILED",
3740
+ `fix did not touch review-named file(s): ${missing.join(", ")} \u2014 address them or mark declined with a reason`,
3741
+ summary,
3742
+ { missingFiles: missing, declinedFiles: Array.from(declined), changedFiles }
3743
+ );
3744
+ }
3716
3745
  }
3717
3746
  if (summary.fixedItems === 0 && summary.declinedItems > 0 && !committed) {
3718
3747
  ctx.data.action = makeAction2("FIX_DECLINED", {
@@ -3721,6 +3750,25 @@ var verifyFixAlignment = async (ctx, profile) => {
3721
3750
  });
3722
3751
  }
3723
3752
  };
3753
+ function failOnce(ctx, type, reason, summary, extra) {
3754
+ ctx.output.exitCode = 1;
3755
+ ctx.output.reason = reason;
3756
+ ctx.data.agentDone = false;
3757
+ ctx.data.action = makeAction2(type, {
3758
+ reason,
3759
+ feedbackActionsSummary: summary,
3760
+ ...extra ?? {}
3761
+ });
3762
+ }
3763
+ function filesMatch(changedPath, reviewRef) {
3764
+ if (changedPath === reviewRef) return true;
3765
+ if (changedPath.endsWith("/" + reviewRef)) return true;
3766
+ if (reviewRef.endsWith("/" + changedPath)) return true;
3767
+ const a = changedPath.split("/");
3768
+ const b = reviewRef.split("/");
3769
+ if (a[a.length - 1] !== b[b.length - 1]) return false;
3770
+ return a.length >= 2 && b.length >= 2 && a[a.length - 2] === b[b.length - 2];
3771
+ }
3724
3772
 
3725
3773
  // src/scripts/watchStalePrsFlow.ts
3726
3774
  function readWatchConfig(ctx) {
@@ -3792,7 +3840,7 @@ var watchStalePrsFlow = async (ctx) => {
3792
3840
  };
3793
3841
 
3794
3842
  // src/scripts/writeRunSummary.ts
3795
- import * as fs16 from "fs";
3843
+ import * as fs14 from "fs";
3796
3844
  var writeRunSummary = async (ctx, profile) => {
3797
3845
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3798
3846
  if (!summaryPath) return;
@@ -3814,7 +3862,7 @@ var writeRunSummary = async (ctx, profile) => {
3814
3862
  if (reason) lines.push(`- **Reason:** ${reason}`);
3815
3863
  lines.push("");
3816
3864
  try {
3817
- fs16.appendFileSync(summaryPath, `${lines.join("\n")}
3865
+ fs14.appendFileSync(summaryPath, `${lines.join("\n")}
3818
3866
  `);
3819
3867
  } catch {
3820
3868
  }
@@ -3962,9 +4010,9 @@ async function runExecutable(profileName, input) {
3962
4010
  data: {},
3963
4011
  output: { exitCode: 0 }
3964
4012
  };
3965
- const ndjsonDir = path14.join(input.cwd, ".kody2");
4013
+ const ndjsonDir = path12.join(input.cwd, ".kody2");
3966
4014
  const invokeAgent = async (prompt) => {
3967
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
4015
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
3968
4016
  const syntheticPath = ctx.data.syntheticPluginPath;
3969
4017
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
3970
4018
  return runAgent({
@@ -4030,17 +4078,17 @@ async function runExecutable(profileName, input) {
4030
4078
  }
4031
4079
  }
4032
4080
  function resolveProfilePath(profileName) {
4033
- const here = path14.dirname(new URL(import.meta.url).pathname);
4081
+ const here = path12.dirname(new URL(import.meta.url).pathname);
4034
4082
  const candidates = [
4035
- path14.join(here, "executables", profileName, "profile.json"),
4083
+ path12.join(here, "executables", profileName, "profile.json"),
4036
4084
  // same-dir sibling (dev)
4037
- path14.join(here, "..", "executables", profileName, "profile.json"),
4085
+ path12.join(here, "..", "executables", profileName, "profile.json"),
4038
4086
  // up one (prod: dist/bin → dist/executables)
4039
- path14.join(here, "..", "src", "executables", profileName, "profile.json")
4087
+ path12.join(here, "..", "src", "executables", profileName, "profile.json")
4040
4088
  // fallback
4041
4089
  ];
4042
4090
  for (const c of candidates) {
4043
- if (fs17.existsSync(c)) return c;
4091
+ if (fs15.existsSync(c)) return c;
4044
4092
  }
4045
4093
  return candidates[0];
4046
4094
  }
@@ -4216,9 +4264,9 @@ function resolveAuthToken(env = process.env) {
4216
4264
  return token;
4217
4265
  }
4218
4266
  function detectPackageManager2(cwd) {
4219
- if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4220
- if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
4221
- if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
4267
+ if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4268
+ if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
4269
+ if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
4222
4270
  return "npm";
4223
4271
  }
4224
4272
  function shellOut(cmd, args, cwd, stream = true) {
@@ -4298,11 +4346,11 @@ function configureGitIdentity(cwd) {
4298
4346
  }
4299
4347
  function postFailureTail(issueNumber, cwd, reason) {
4300
4348
  if (!issueNumber) return;
4301
- const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
4349
+ const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
4302
4350
  let tail = "";
4303
4351
  try {
4304
- if (fs18.existsSync(logPath)) {
4305
- const content = fs18.readFileSync(logPath, "utf-8");
4352
+ if (fs16.existsSync(logPath)) {
4353
+ const content = fs16.readFileSync(logPath, "utf-8");
4306
4354
  tail = content.slice(-3e3);
4307
4355
  }
4308
4356
  } catch {
@@ -4327,7 +4375,7 @@ async function runCi(argv) {
4327
4375
  return 0;
4328
4376
  }
4329
4377
  const args = parseCiArgs(argv);
4330
- const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
4378
+ const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
4331
4379
  let earlyConfig;
4332
4380
  try {
4333
4381
  earlyConfig = loadConfig(cwd);
@@ -4420,23 +4468,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
4420
4468
  var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
4421
4469
 
4422
4470
  Usage:
4423
- kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
4471
+ kody2 chat [--session <id>] [--model <provider/model>]
4424
4472
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
4425
4473
 
4426
- All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
4427
- CLI flags take precedence over env. SESSION_ID is required.
4474
+ All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
4475
+ CLI flags take precedence over env. SESSION_ID and DASHBOARD_URL are required
4476
+ (the runner long-polls the dashboard for user turns and pushes events back).
4428
4477
 
4429
4478
  Exit codes:
4430
- 0 reply emitted successfully
4431
- 64 bad inputs (missing session, empty history)
4432
- 99 runtime failure (agent crash, LiteLLM failure)
4479
+ 0 session exited cleanly (idle or hard timeout)
4480
+ 64 bad inputs
4481
+ 99 runtime failure (agent crash, pull failure, LiteLLM failure)
4433
4482
  `;
4434
4483
  function parseChatArgs(argv, env = process.env) {
4435
4484
  const result = { errors: [] };
4436
4485
  for (let i = 0; i < argv.length; i++) {
4437
4486
  const arg = argv[i];
4438
4487
  if (arg === "--session") result.sessionId = argv[++i];
4439
- else if (arg === "--message") result.initMessage = argv[++i];
4440
4488
  else if (arg === "--model") result.model = argv[++i];
4441
4489
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
4442
4490
  else if (arg === "--cwd") result.cwd = argv[++i];
@@ -4447,34 +4495,18 @@ function parseChatArgs(argv, env = process.env) {
4447
4495
  else if (arg) result.errors.push(`unexpected positional: ${arg}`);
4448
4496
  }
4449
4497
  result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
4450
- result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
4451
4498
  result.model = result.model ?? env.MODEL ?? void 0;
4452
4499
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
4453
- for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
4500
+ for (const key of ["sessionId", "model", "dashboardUrl"]) {
4454
4501
  const v = result[key];
4455
4502
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
4456
4503
  }
4457
- if (!result.sessionId && !result.errors.includes("__HELP__")) {
4458
- result.errors.push("--session <id> (or SESSION_ID env) is required");
4504
+ if (!result.errors.includes("__HELP__")) {
4505
+ if (!result.sessionId) result.errors.push("--session <id> (or SESSION_ID env) is required");
4506
+ if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
4459
4507
  }
4460
4508
  return result;
4461
4509
  }
4462
- function commitChatFiles(cwd, sessionId, verbose) {
4463
- const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
4464
- const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
4465
- const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
4466
- if (paths.length === 0) return;
4467
- const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
4468
- try {
4469
- execFileSync16("git", ["add", ...paths], opts);
4470
- execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
4471
- execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
4472
- } catch (err) {
4473
- const msg = err instanceof Error ? err.message : String(err);
4474
- process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
4475
- `);
4476
- }
4477
- }
4478
4510
  function tryLoadConfig(cwd) {
4479
4511
  try {
4480
4512
  return loadConfig(cwd);
@@ -4482,11 +4514,6 @@ function tryLoadConfig(cwd) {
4482
4514
  return null;
4483
4515
  }
4484
4516
  }
4485
- function buildSink(cwd, sessionId, dashboardUrl) {
4486
- const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
4487
- if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
4488
- return new TeeSink(sinks);
4489
- }
4490
4517
  async function runChat(argv) {
4491
4518
  if (argv.includes("--help") || argv.includes("-h")) {
4492
4519
  process.stdout.write(CHAT_HELP);
@@ -4500,8 +4527,9 @@ async function runChat(argv) {
4500
4527
  ${CHAT_HELP}`);
4501
4528
  return 64;
4502
4529
  }
4503
- const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
4530
+ const cwd = args.cwd ? path14.resolve(args.cwd) : process.cwd();
4504
4531
  const sessionId = args.sessionId;
4532
+ const dashboardUrl = args.dashboardUrl;
4505
4533
  const unpackedSecrets = unpackAllSecrets();
4506
4534
  if (unpackedSecrets > 0) {
4507
4535
  process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
@@ -4527,13 +4555,20 @@ ${CHAT_HELP}`);
4527
4555
  return 99;
4528
4556
  }
4529
4557
  }
4558
+ let sink;
4559
+ try {
4560
+ sink = new HttpSink(dashboardUrl, sessionId);
4561
+ } catch (err) {
4562
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4563
+ `);
4564
+ return 64;
4565
+ }
4530
4566
  let litellm = null;
4531
4567
  try {
4532
4568
  litellm = await startLitellmIfNeeded(model, cwd);
4533
4569
  } catch (err) {
4534
4570
  const msg = err instanceof Error ? err.message : String(err);
4535
- const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
4536
- await sink2.emit({
4571
+ await sink.emit({
4537
4572
  event: "chat.error",
4538
4573
  payload: { sessionId, error: `litellm startup failed: ${msg}` },
4539
4574
  runId: makeRunId(sessionId, "error"),
@@ -4541,21 +4576,33 @@ ${CHAT_HELP}`);
4541
4576
  });
4542
4577
  return 99;
4543
4578
  }
4544
- const sessionFile = sessionFilePath(cwd, sessionId);
4545
- if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
4546
- const sink = buildSink(cwd, sessionId, args.dashboardUrl);
4579
+ let pull;
4547
4580
  try {
4548
- const result = await runChatTurn({
4581
+ pull = createPullClient({ baseUrl: dashboardUrl, sessionId });
4582
+ } catch (err) {
4583
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4584
+ `);
4585
+ try {
4586
+ litellm?.kill();
4587
+ } catch {
4588
+ }
4589
+ return 64;
4590
+ }
4591
+ process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
4592
+ `);
4593
+ try {
4594
+ const result = await runChatSession({
4549
4595
  sessionId,
4550
- sessionFile,
4551
4596
  cwd,
4552
4597
  model,
4553
4598
  litellmUrl: litellm?.url ?? null,
4554
4599
  sink,
4600
+ pull,
4555
4601
  verbose: args.verbose,
4556
4602
  quiet: args.quiet
4557
4603
  });
4558
- commitChatFiles(cwd, sessionId, args.verbose ?? false);
4604
+ process.stdout.write(`\u2192 kody2 chat: exited (${result.reason ?? "ok"}) after ${result.turnsProcessed} turn(s)
4605
+ `);
4559
4606
  return result.exitCode;
4560
4607
  } finally {
4561
4608
  try {