@kody-ade/kody-engine 0.2.22 → 0.2.26

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