@kody-ade/kody-engine 0.2.21 → 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.21",
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;
@@ -2479,6 +2789,28 @@ function postWith(type, n, body, cwd) {
2479
2789
  }
2480
2790
  }
2481
2791
 
2792
+ // src/scripts/postPlanComment.ts
2793
+ var postPlanComment = async (ctx) => {
2794
+ if (!ctx.data.agentDone) return;
2795
+ const targetType = ctx.data.commentTargetType;
2796
+ const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
2797
+ const plan = ctx.data.prSummary?.trim();
2798
+ if (targetType !== "issue" || !targetNumber || !plan) return;
2799
+ const body = renderPlanComment(targetNumber, plan);
2800
+ try {
2801
+ postIssueComment(targetNumber, body, ctx.cwd);
2802
+ } catch {
2803
+ }
2804
+ };
2805
+ function renderPlanComment(issueNumber, plan) {
2806
+ return `## Plan for issue #${issueNumber}
2807
+
2808
+ ${plan}
2809
+
2810
+ ---
2811
+ Comment \`@kody2 run\` to execute this plan.`;
2812
+ }
2813
+
2482
2814
  // src/scripts/postReviewResult.ts
2483
2815
  function detectVerdict(body) {
2484
2816
  const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
@@ -2523,15 +2855,17 @@ var postReviewResult = async (ctx, _profile, agentResult) => {
2523
2855
  const verdict = detectVerdict(reviewBody);
2524
2856
  ctx.data.reviewVerdict = verdict;
2525
2857
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
2526
- process.stdout.write(`
2858
+ process.stdout.write(
2859
+ `
2527
2860
  REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
2528
- `);
2861
+ `
2862
+ );
2529
2863
  };
2530
2864
 
2531
2865
  // src/scripts/releaseFlow.ts
2532
2866
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2533
- import * as fs12 from "fs";
2534
- import * as path11 from "path";
2867
+ import * as fs15 from "fs";
2868
+ import * as path13 from "path";
2535
2869
  function bumpVersion(current, bump) {
2536
2870
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2537
2871
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2547,12 +2881,12 @@ function bumpVersion(current, bump) {
2547
2881
  return `${major}.${minor}.${patch}`;
2548
2882
  }
2549
2883
  function updateVersionInFile(file, newVersion, cwd) {
2550
- const abs = path11.join(cwd, file);
2551
- if (!fs12.existsSync(abs)) return false;
2552
- 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");
2553
2887
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2554
2888
  if (updated === content) return false;
2555
- fs12.writeFileSync(abs, updated);
2889
+ fs15.writeFileSync(abs, updated);
2556
2890
  return true;
2557
2891
  }
2558
2892
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2600,19 +2934,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2600
2934
  return parts.join("\n");
2601
2935
  }
2602
2936
  function prependChangelog(cwd, entry) {
2603
- const p = path11.join(cwd, "CHANGELOG.md");
2937
+ const p = path13.join(cwd, "CHANGELOG.md");
2604
2938
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2605
- if (fs12.existsSync(p)) {
2606
- const prior = fs12.readFileSync(p, "utf-8");
2939
+ if (fs15.existsSync(p)) {
2940
+ const prior = fs15.readFileSync(p, "utf-8");
2607
2941
  if (/^#\s*Changelog\b/m.test(prior)) {
2608
2942
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2609
- fs12.writeFileSync(p, `${prior.slice(0, idx + 1)}
2943
+ fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
2610
2944
  ${entry}${prior.slice(idx + 1)}`);
2611
2945
  } else {
2612
- fs12.writeFileSync(p, `${header}${entry}${prior}`);
2946
+ fs15.writeFileSync(p, `${header}${entry}${prior}`);
2613
2947
  }
2614
2948
  } else {
2615
- fs12.writeFileSync(p, `${header}${entry}`);
2949
+ fs15.writeFileSync(p, `${header}${entry}`);
2616
2950
  }
2617
2951
  }
2618
2952
  function git3(args, cwd, timeout = 6e4) {
@@ -2663,13 +2997,13 @@ var releaseFlow = async (ctx) => {
2663
2997
  };
2664
2998
  async function runPrepare(args) {
2665
2999
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
2666
- const pkgPath = path11.join(cwd, "package.json");
2667
- if (!fs12.existsSync(pkgPath)) {
3000
+ const pkgPath = path13.join(cwd, "package.json");
3001
+ if (!fs15.existsSync(pkgPath)) {
2668
3002
  ctx.output.exitCode = 99;
2669
3003
  ctx.output.reason = "release prepare: package.json not found";
2670
3004
  return;
2671
3005
  }
2672
- const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
3006
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
2673
3007
  if (typeof pkg.version !== "string") {
2674
3008
  ctx.output.exitCode = 99;
2675
3009
  ctx.output.reason = "release prepare: package.json has no version";
@@ -2723,10 +3057,10 @@ ${entry}
2723
3057
  Merge this and then run \`kody2 release --mode finalize\`.`;
2724
3058
  let prUrl = "";
2725
3059
  try {
2726
- prUrl = gh(
2727
- ["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"],
2728
- { input: body, cwd }
2729
- ).trim();
3060
+ prUrl = gh(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
3061
+ input: body,
3062
+ cwd
3063
+ }).trim();
2730
3064
  } catch (err) {
2731
3065
  const msg = err instanceof Error ? err.message : String(err);
2732
3066
  ctx.output.exitCode = 4;
@@ -2740,8 +3074,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
2740
3074
  }
2741
3075
  async function runFinalize(args) {
2742
3076
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2743
- const pkgPath = path11.join(cwd, "package.json");
2744
- 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"));
2745
3079
  if (typeof pkg.version !== "string") {
2746
3080
  ctx.output.exitCode = 99;
2747
3081
  ctx.output.reason = "release finalize: package.json has no version";
@@ -2798,20 +3132,14 @@ ${truncate2(r.stderr, 2e3)}
2798
3132
  }
2799
3133
  let releaseUrl = "";
2800
3134
  try {
2801
- const releaseArgs = [
2802
- "release",
2803
- "create",
2804
- tag,
2805
- "--title",
2806
- tag,
2807
- "--notes",
2808
- `Release ${tag} \u2014 automated by kody2.`
2809
- ];
3135
+ const releaseArgs = ["release", "create", tag, "--title", tag, "--notes", `Release ${tag} \u2014 automated by kody2.`];
2810
3136
  if (releaseCfg.draftRelease) releaseArgs.push("--draft");
2811
3137
  releaseUrl = gh(releaseArgs, { cwd }).trim();
2812
3138
  } catch (err) {
2813
- process.stderr.write(`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
2814
- `);
3139
+ process.stderr.write(
3140
+ `[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
3141
+ `
3142
+ );
2815
3143
  }
2816
3144
  if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
2817
3145
  const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
@@ -2830,6 +3158,35 @@ ${truncate2(r.stderr, 2e3)}
2830
3158
  `);
2831
3159
  }
2832
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
+
2833
3190
  // src/scripts/resolveArtifacts.ts
2834
3191
  var resolveArtifacts = async (ctx, profile) => {
2835
3192
  if (profile.inputArtifacts.length === 0) return;
@@ -3079,11 +3436,7 @@ var syncFlow = async (ctx) => {
3079
3436
  ctx.output.reason = `merged origin/${baseBranch} into ${ctx.data.branch}`;
3080
3437
  const runUrl = getRunUrl();
3081
3438
  const runSuffix = runUrl ? ` ([logs](${runUrl}))` : "";
3082
- tryPostPr5(
3083
- prNumber,
3084
- `\u2705 kody2 sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`,
3085
- ctx.cwd
3086
- );
3439
+ tryPostPr5(prNumber, `\u2705 kody2 sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`, ctx.cwd);
3087
3440
  };
3088
3441
  function bail2(ctx, prNumber, reason) {
3089
3442
  ctx.output.exitCode = 1;
@@ -3123,7 +3476,7 @@ import { spawn as spawn2 } from "child_process";
3123
3476
  var TAIL_CHARS = 4e3;
3124
3477
  var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
3125
3478
  function runCommand(command, cwd) {
3126
- return new Promise((resolve3) => {
3479
+ return new Promise((resolve4) => {
3127
3480
  const start = Date.now();
3128
3481
  const child = spawn2(command, {
3129
3482
  cwd,
@@ -3152,11 +3505,11 @@ function runCommand(command, cwd) {
3152
3505
  child.on("exit", (code) => {
3153
3506
  clearTimeout(timer);
3154
3507
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
3155
- resolve3({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
3508
+ resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
3156
3509
  });
3157
3510
  child.on("error", (err) => {
3158
3511
  clearTimeout(timer);
3159
- resolve3({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
3512
+ resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
3160
3513
  });
3161
3514
  });
3162
3515
  }
@@ -3215,19 +3568,7 @@ function readWatchConfig(ctx) {
3215
3568
  function findStalePrs(cwd, staleDays, now = /* @__PURE__ */ new Date()) {
3216
3569
  let raw = "";
3217
3570
  try {
3218
- raw = gh(
3219
- [
3220
- "pr",
3221
- "list",
3222
- "--state",
3223
- "open",
3224
- "--limit",
3225
- "100",
3226
- "--json",
3227
- "number,title,url,updatedAt"
3228
- ],
3229
- { cwd }
3230
- );
3571
+ raw = gh(["pr", "list", "--state", "open", "--limit", "100", "--json", "number,title,url,updatedAt"], { cwd });
3231
3572
  } catch {
3232
3573
  return [];
3233
3574
  }
@@ -3273,8 +3614,10 @@ var watchStalePrsFlow = async (ctx) => {
3273
3614
  try {
3274
3615
  postIssueComment(reportIssueNumber, report, ctx.cwd);
3275
3616
  } catch (err) {
3276
- process.stderr.write(`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
3277
- `);
3617
+ process.stderr.write(
3618
+ `[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
3619
+ `
3620
+ );
3278
3621
  }
3279
3622
  }
3280
3623
  ctx.output.exitCode = 0;
@@ -3282,7 +3625,7 @@ var watchStalePrsFlow = async (ctx) => {
3282
3625
  };
3283
3626
 
3284
3627
  // src/scripts/writeRunSummary.ts
3285
- import * as fs13 from "fs";
3628
+ import * as fs16 from "fs";
3286
3629
  var writeRunSummary = async (ctx, profile) => {
3287
3630
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3288
3631
  if (!summaryPath) return;
@@ -3304,7 +3647,7 @@ var writeRunSummary = async (ctx, profile) => {
3304
3647
  if (reason) lines.push(`- **Reason:** ${reason}`);
3305
3648
  lines.push("");
3306
3649
  try {
3307
- fs13.appendFileSync(summaryPath, `${lines.join("\n")}
3650
+ fs16.appendFileSync(summaryPath, `${lines.join("\n")}
3308
3651
  `);
3309
3652
  } catch {
3310
3653
  }
@@ -3331,11 +3674,13 @@ var preflightScripts = {
3331
3674
  };
3332
3675
  var postflightScripts = {
3333
3676
  parseAgentResult: parseAgentResult2,
3677
+ requireFeedbackActions,
3334
3678
  verify,
3335
3679
  checkCoverageWithRetry,
3336
3680
  commitAndPush: commitAndPush2,
3337
3681
  ensurePr: ensurePr2,
3338
3682
  postIssueComment: postIssueComment2,
3683
+ postPlanComment,
3339
3684
  postReviewResult,
3340
3685
  persistArtifacts,
3341
3686
  writeRunSummary,
@@ -3448,9 +3793,9 @@ async function runExecutable(profileName, input) {
3448
3793
  data: {},
3449
3794
  output: { exitCode: 0 }
3450
3795
  };
3451
- const ndjsonDir = path12.join(input.cwd, ".kody2");
3796
+ const ndjsonDir = path14.join(input.cwd, ".kody2");
3452
3797
  const invokeAgent = async (prompt) => {
3453
- 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);
3454
3799
  const syntheticPath = ctx.data.syntheticPluginPath;
3455
3800
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
3456
3801
  return runAgent({
@@ -3466,6 +3811,7 @@ async function runExecutable(profileName, input) {
3466
3811
  mcpServers: profile.claudeCode.mcpServers,
3467
3812
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
3468
3813
  maxTurns: profile.claudeCode.maxTurns,
3814
+ maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
3469
3815
  systemPromptAppend: profile.claudeCode.systemPromptAppend,
3470
3816
  settingSources: profile.claudeCode.settingSources
3471
3817
  });
@@ -3516,17 +3862,17 @@ async function runExecutable(profileName, input) {
3516
3862
  }
3517
3863
  }
3518
3864
  function resolveProfilePath(profileName) {
3519
- const here = path12.dirname(new URL(import.meta.url).pathname);
3865
+ const here = path14.dirname(new URL(import.meta.url).pathname);
3520
3866
  const candidates = [
3521
- path12.join(here, "executables", profileName, "profile.json"),
3867
+ path14.join(here, "executables", profileName, "profile.json"),
3522
3868
  // same-dir sibling (dev)
3523
- path12.join(here, "..", "executables", profileName, "profile.json"),
3869
+ path14.join(here, "..", "executables", profileName, "profile.json"),
3524
3870
  // up one (prod: dist/bin → dist/executables)
3525
- path12.join(here, "..", "src", "executables", profileName, "profile.json")
3871
+ path14.join(here, "..", "src", "executables", profileName, "profile.json")
3526
3872
  // fallback
3527
3873
  ];
3528
3874
  for (const c of candidates) {
3529
- if (fs14.existsSync(c)) return c;
3875
+ if (fs17.existsSync(c)) return c;
3530
3876
  }
3531
3877
  return candidates[0];
3532
3878
  }
@@ -3617,95 +3963,6 @@ function finish(out) {
3617
3963
  return out;
3618
3964
  }
3619
3965
 
3620
- // src/kody2-cli.ts
3621
- import { execFileSync as execFileSync15 } from "child_process";
3622
- import * as fs16 from "fs";
3623
- import * as path13 from "path";
3624
-
3625
- // src/dispatch.ts
3626
- import * as fs15 from "fs";
3627
- function autoDispatch(opts) {
3628
- const explicit = opts?.explicit;
3629
- if (explicit?.issueNumber && explicit.issueNumber > 0) {
3630
- return {
3631
- executable: "run",
3632
- cliArgs: { issue: explicit.issueNumber },
3633
- target: explicit.issueNumber
3634
- };
3635
- }
3636
- const eventName = process.env.GITHUB_EVENT_NAME;
3637
- const eventPath = process.env.GITHUB_EVENT_PATH;
3638
- if (!eventName || !eventPath || !fs15.existsSync(eventPath)) return null;
3639
- let event = {};
3640
- try {
3641
- event = JSON.parse(fs15.readFileSync(eventPath, "utf-8"));
3642
- } catch {
3643
- return null;
3644
- }
3645
- if (eventName === "workflow_dispatch") {
3646
- const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
3647
- if (!Number.isNaN(n) && n > 0) {
3648
- return { executable: "run", cliArgs: { issue: n }, target: n };
3649
- }
3650
- return null;
3651
- }
3652
- if (eventName !== "issue_comment") return null;
3653
- const body = String(event.comment?.body ?? "").toLowerCase();
3654
- const targetNum = Number(event.issue?.number ?? 0);
3655
- const isPr = !!event.issue?.pull_request;
3656
- if (!targetNum) return null;
3657
- const afterTag = extractAfterTag(body);
3658
- if (isPr) {
3659
- if (/\bfix-ci\b/.test(afterTag)) {
3660
- return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
3661
- }
3662
- if (/\bresolve\b/.test(afterTag)) {
3663
- return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
3664
- }
3665
- if (/\breview\b/.test(afterTag)) {
3666
- return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
3667
- }
3668
- if (/\bsync\b/.test(afterTag)) {
3669
- return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
3670
- }
3671
- const feedback = extractFeedback(afterTag);
3672
- return {
3673
- executable: "fix",
3674
- cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
3675
- target: targetNum
3676
- };
3677
- }
3678
- const sub = extractSubcommand(afterTag);
3679
- const defaultExec = opts?.config?.defaultExecutable ?? "run";
3680
- if (!sub) {
3681
- return asDispatch(defaultExec, targetNum);
3682
- }
3683
- if (sub === "orchestrate" || sub === "orchestrator") {
3684
- return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
3685
- }
3686
- if (sub === "build") {
3687
- return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
3688
- }
3689
- return asDispatch(sub, targetNum);
3690
- }
3691
- function asDispatch(executable, target) {
3692
- return { executable, cliArgs: { issue: target }, target };
3693
- }
3694
- function extractAfterTag(body) {
3695
- const idx = body.indexOf("@kody2");
3696
- if (idx === -1) return body;
3697
- return body.slice(idx + "@kody2".length).trim();
3698
- }
3699
- function extractSubcommand(afterTag) {
3700
- const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
3701
- if (!match) return null;
3702
- return match[1];
3703
- }
3704
- function extractFeedback(afterTag) {
3705
- const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
3706
- return cleaned.length > 0 ? cleaned : void 0;
3707
- }
3708
-
3709
3966
  // src/kody2-cli.ts
3710
3967
  var CI_HELP = `kody2 ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
3711
3968
 
@@ -3791,9 +4048,9 @@ function resolveAuthToken(env = process.env) {
3791
4048
  return token;
3792
4049
  }
3793
4050
  function detectPackageManager2(cwd) {
3794
- if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3795
- if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
3796
- 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";
3797
4054
  return "npm";
3798
4055
  }
3799
4056
  function shellOut(cmd, args, cwd, stream = true) {
@@ -3873,11 +4130,11 @@ function configureGitIdentity(cwd) {
3873
4130
  }
3874
4131
  function postFailureTail(issueNumber, cwd, reason) {
3875
4132
  if (!issueNumber) return;
3876
- const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
4133
+ const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
3877
4134
  let tail = "";
3878
4135
  try {
3879
- if (fs16.existsSync(logPath)) {
3880
- const content = fs16.readFileSync(logPath, "utf-8");
4136
+ if (fs18.existsSync(logPath)) {
4137
+ const content = fs18.readFileSync(logPath, "utf-8");
3881
4138
  tail = content.slice(-3e3);
3882
4139
  }
3883
4140
  } catch {
@@ -3902,7 +4159,7 @@ async function runCi(argv) {
3902
4159
  return 0;
3903
4160
  }
3904
4161
  const args = parseCiArgs(argv);
3905
- const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
4162
+ const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
3906
4163
  let earlyConfig;
3907
4164
  try {
3908
4165
  earlyConfig = loadConfig(cwd);
@@ -3990,6 +4247,148 @@ ${CI_HELP}`);
3990
4247
  }
3991
4248
  }
3992
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
+
3993
4392
  // src/entry.ts
3994
4393
  var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
3995
4394
 
@@ -4001,6 +4400,7 @@ Usage:
4001
4400
  kody2 review --pr <N> [--cwd <path>] [--verbose|--quiet]
4002
4401
  kody2 <other> [--cwd <path>] [--verbose|--quiet]
4003
4402
  kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
4403
+ kody2 chat [chat flags \u2014 see: kody2 chat --help]
4004
4404
  kody2 help
4005
4405
  kody2 version
4006
4406
 
@@ -4027,6 +4427,9 @@ function parseArgs(argv) {
4027
4427
  if (cmd === "ci") {
4028
4428
  return { ...result, command: "ci", ciArgv: argv.slice(1) };
4029
4429
  }
4430
+ if (cmd === "chat") {
4431
+ return { ...result, command: "chat", chatArgv: argv.slice(1) };
4432
+ }
4030
4433
  if (hasExecutable(cmd)) {
4031
4434
  result.command = "__executable__";
4032
4435
  result.executableName = cmd;
@@ -4067,6 +4470,18 @@ ${HELP_TEXT}`);
4067
4470
  process.stderr.write(`[kody2] fatal: ${msg}
4068
4471
  `);
4069
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}
4070
4485
  `);
4071
4486
  return 99;
4072
4487
  }