@kody-ade/kody-engine 0.2.31 → 0.2.33

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.31",
6
+ version: "0.2.33",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -50,81 +50,115 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import { execFileSync as execFileSync16 } from "child_process";
54
- import * as fs19 from "fs";
55
- import * as path16 from "path";
53
+ import * as path14 from "path";
56
54
 
57
- // src/chat/events.ts
58
- import * as fs from "fs";
59
- import * as path from "path";
60
- function eventsFilePath(cwd, sessionId) {
61
- return path.join(cwd, ".kody", "events", `${sessionId}.jsonl`);
55
+ // src/chat/pull.ts
56
+ var PullError = class extends Error {
57
+ constructor(message, status) {
58
+ super(message);
59
+ this.status = status;
60
+ this.name = "PullError";
61
+ }
62
+ status;
63
+ };
64
+ function createPullClient(opts) {
65
+ const fetchFn = opts.fetchFn ?? fetch;
66
+ const parsed = parseUrl(opts.baseUrl);
67
+ const token = opts.token ?? parsed.token;
68
+ if (!token) {
69
+ throw new PullError("session token not provided (expected inline ?token= in dashboardUrl)");
70
+ }
71
+ return async function pull(since, timeoutMs) {
72
+ const url = new URL(parsed.origin);
73
+ url.pathname = "/api/kody/chat/pull";
74
+ url.searchParams.set("sessionId", opts.sessionId);
75
+ url.searchParams.set("since", String(since));
76
+ url.searchParams.set("timeoutMs", String(timeoutMs));
77
+ url.searchParams.set("token", token);
78
+ const abort = AbortSignal.timeout(timeoutMs + 1e4);
79
+ const res = await fetchFn(url.toString(), {
80
+ method: "GET",
81
+ headers: { authorization: `Bearer ${token}` },
82
+ signal: abort
83
+ });
84
+ if (!res.ok) {
85
+ const body = await res.text().catch(() => "");
86
+ throw new PullError(`pull ${url.pathname} \u2192 ${res.status}: ${body.slice(0, 200)}`, res.status);
87
+ }
88
+ const data = await res.json();
89
+ return data;
90
+ };
62
91
  }
63
- var FileSink = class {
64
- constructor(file) {
65
- this.file = file;
66
- }
67
- file;
68
- async emit(event) {
69
- fs.mkdirSync(path.dirname(this.file), { recursive: true });
70
- fs.appendFileSync(this.file, `${JSON.stringify(event)}
71
- `);
92
+ function parseUrl(baseUrl) {
93
+ try {
94
+ const u = new URL(baseUrl);
95
+ const token = u.searchParams.get("token");
96
+ const origin = `${u.protocol}//${u.host}${u.pathname !== "/" ? u.pathname : ""}`.replace(/\/$/, "");
97
+ return { origin: origin || u.origin, token };
98
+ } catch {
99
+ return { origin: baseUrl, token: null };
72
100
  }
73
- };
101
+ }
102
+
103
+ // src/chat/events.ts
74
104
  var HttpSink = class {
75
- constructor(baseUrl, sessionId, logger = {
105
+ constructor(baseUrl, sessionId, token, fetchFn = fetch, logger = {
76
106
  warn: (m) => process.stderr.write(`[kody2:chat] ${m}
77
107
  `)
78
108
  }) {
79
- this.baseUrl = baseUrl;
80
109
  this.sessionId = sessionId;
110
+ this.fetchFn = fetchFn;
81
111
  this.logger = logger;
112
+ const parsed = parseUrl(baseUrl);
113
+ this.origin = parsed.origin;
114
+ const resolved = token ?? parsed.token;
115
+ if (!resolved) {
116
+ throw new Error("HttpSink: session token not provided (expected inline ?token= in baseUrl)");
117
+ }
118
+ this.token = resolved;
82
119
  }
83
- baseUrl;
84
120
  sessionId;
121
+ fetchFn;
85
122
  logger;
123
+ origin;
124
+ token;
86
125
  async emit(event) {
87
- const url = withSessionParam(this.baseUrl, this.sessionId);
126
+ const url = new URL(this.origin);
127
+ url.pathname = "/api/kody/events/ingest";
128
+ url.searchParams.set("sessionId", this.sessionId);
129
+ url.searchParams.set("token", this.token);
88
130
  try {
89
- const res = await fetch(url, {
131
+ const res = await this.fetchFn(url.toString(), {
90
132
  method: "POST",
91
- headers: { "content-type": "application/json" },
133
+ headers: {
134
+ "content-type": "application/json",
135
+ authorization: `Bearer ${this.token}`
136
+ },
92
137
  body: JSON.stringify(event),
93
138
  signal: AbortSignal.timeout(5e3)
94
139
  });
95
140
  if (!res.ok) {
96
- this.logger.warn(`HttpSink POST ${url} \u2192 ${res.status}`);
141
+ this.logger.warn(`HttpSink POST ${url.pathname} \u2192 ${res.status}`);
97
142
  }
98
143
  } catch (err) {
99
- this.logger.warn(`HttpSink POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
144
+ this.logger.warn(
145
+ `HttpSink POST ${url.pathname} failed: ${err instanceof Error ? err.message : String(err)}`
146
+ );
100
147
  }
101
148
  }
102
149
  };
103
- var TeeSink = class {
104
- constructor(sinks) {
105
- this.sinks = sinks;
106
- }
107
- sinks;
108
- async emit(event) {
109
- await Promise.all(this.sinks.map((s) => s.emit(event)));
110
- }
111
- };
112
- function withSessionParam(baseUrl, sessionId) {
113
- const joiner = baseUrl.includes("?") ? "&" : "?";
114
- return `${baseUrl}${joiner}sessionId=${encodeURIComponent(sessionId)}`;
115
- }
116
150
  function makeRunId(sessionId, suffix) {
117
151
  return `chat-${sessionId}-${suffix}`;
118
152
  }
119
153
 
120
154
  // src/agent.ts
121
- import * as fs3 from "fs";
122
- import * as path3 from "path";
155
+ import * as fs2 from "fs";
156
+ import * as path2 from "path";
123
157
  import { query } from "@anthropic-ai/claude-agent-sdk";
124
158
 
125
159
  // src/config.ts
126
- import * as fs2 from "fs";
127
- import * as path2 from "path";
160
+ import * as fs from "fs";
161
+ import * as path from "path";
128
162
  var LITELLM_DEFAULT_PORT = 4e3;
129
163
  var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
130
164
  function parseProviderModel(s) {
@@ -142,13 +176,13 @@ function needsLitellmProxy(model) {
142
176
  return model.provider !== "claude" && model.provider !== "anthropic";
143
177
  }
144
178
  function loadConfig(projectDir = process.cwd()) {
145
- const configPath = path2.join(projectDir, "kody.config.json");
146
- if (!fs2.existsSync(configPath)) {
179
+ const configPath = path.join(projectDir, "kody.config.json");
180
+ if (!fs.existsSync(configPath)) {
147
181
  throw new Error(`kody.config.json not found at ${configPath}`);
148
182
  }
149
183
  let raw;
150
184
  try {
151
- raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
185
+ raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
152
186
  } catch (err) {
153
187
  const msg = err instanceof Error ? err.message : String(err);
154
188
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
@@ -325,10 +359,10 @@ function formatBytes(bytes) {
325
359
  // src/agent.ts
326
360
  var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
327
361
  async function runAgent(opts) {
328
- const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody2");
329
- fs3.mkdirSync(ndjsonDir, { recursive: true });
330
- const ndjsonPath = path3.join(ndjsonDir, "last-run.jsonl");
331
- const fullLog = fs3.createWriteStream(ndjsonPath, { flags: "w" });
362
+ const ndjsonDir = opts.ndjsonDir ?? path2.join(opts.cwd, ".kody2");
363
+ fs2.mkdirSync(ndjsonDir, { recursive: true });
364
+ const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
365
+ const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
332
366
  const env = {
333
367
  ...process.env,
334
368
  SKIP_HOOKS: "1",
@@ -359,6 +393,9 @@ async function runAgent(opts) {
359
393
  if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
360
394
  queryOptions.maxTurns = opts.maxTurns;
361
395
  }
396
+ if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
397
+ queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
398
+ }
362
399
  if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
363
400
  queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
364
401
  }
@@ -400,53 +437,6 @@ async function runAgent(opts) {
400
437
  return { outcome, finalText, error: errorMessage, ndjsonPath };
401
438
  }
402
439
 
403
- // src/chat/session.ts
404
- import * as fs4 from "fs";
405
- import * as path4 from "path";
406
- function sessionFilePath(cwd, sessionId) {
407
- return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
408
- }
409
- function readSession(file) {
410
- if (!fs4.existsSync(file)) return [];
411
- const raw = fs4.readFileSync(file, "utf-8").trim();
412
- if (!raw) return [];
413
- const turns = [];
414
- for (const line of raw.split("\n")) {
415
- if (!line.trim()) continue;
416
- try {
417
- const parsed = JSON.parse(line);
418
- if (parsed.role !== "user" && parsed.role !== "assistant") continue;
419
- if (typeof parsed.content !== "string") continue;
420
- turns.push(parsed);
421
- } catch {
422
- }
423
- }
424
- return turns;
425
- }
426
- function appendTurn(file, turn) {
427
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
428
- const line = JSON.stringify({
429
- role: turn.role,
430
- content: turn.content,
431
- timestamp: turn.timestamp,
432
- toolCalls: turn.toolCalls ?? []
433
- });
434
- fs4.appendFileSync(file, `${line}
435
- `);
436
- }
437
- function seedInitialMessage(file, message) {
438
- if (!message.trim()) return false;
439
- const turns = readSession(file);
440
- const lastUser = [...turns].reverse().find((t) => t.role === "user");
441
- if (lastUser && lastUser.content === message) return false;
442
- appendTurn(file, {
443
- role: "user",
444
- content: message,
445
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
446
- });
447
- return true;
448
- }
449
-
450
440
  // src/chat/loop.ts
451
441
  var CHAT_SYSTEM_PROMPT = [
452
442
  "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
@@ -455,20 +445,15 @@ var CHAT_SYSTEM_PROMPT = [
455
445
  "read repository code or execute small checks when it helps you answer \u2014 otherwise",
456
446
  "reply directly. Do not invent file paths, commit SHAs, or command output."
457
447
  ].join("\n");
458
- async function runChatTurn(opts) {
459
- const turns = readSession(opts.sessionFile);
460
- if (turns.length === 0) {
461
- const error = "session file is empty \u2014 nothing to reply to";
462
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
463
- return { exitCode: 64, error };
464
- }
465
- const lastTurn = turns[turns.length - 1];
466
- if (lastTurn.role !== "user") {
467
- const error = "last turn is not a user message \u2014 assistant already replied";
468
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
469
- return { exitCode: 64, error };
470
- }
471
- const prompt = buildPrompt(turns, opts.systemPrompt ?? CHAT_SYSTEM_PROMPT);
448
+ var DEFAULT_IDLE_TIMEOUT_MS = 3 * 60 * 1e3;
449
+ var DEFAULT_HARD_TIMEOUT_MS = 5 * 60 * 60 * 1e3;
450
+ var DEFAULT_PULL_TIMEOUT_MS = 25e3;
451
+ async function runChatSession(opts) {
452
+ const now = opts.now ?? (() => Date.now());
453
+ const idleTimeoutMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
454
+ const hardTimeoutMs = opts.hardTimeoutMs ?? DEFAULT_HARD_TIMEOUT_MS;
455
+ const pullTimeoutMs = opts.pullTimeoutMs ?? DEFAULT_PULL_TIMEOUT_MS;
456
+ const systemPrompt = opts.systemPrompt ?? CHAT_SYSTEM_PROMPT;
472
457
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
473
458
  prompt: p,
474
459
  model: opts.model,
@@ -477,34 +462,68 @@ async function runChatTurn(opts) {
477
462
  verbose: opts.verbose,
478
463
  quiet: opts.quiet
479
464
  }));
480
- let result;
481
- try {
482
- result = await invoke(prompt);
483
- } catch (err) {
484
- const error = err instanceof Error ? err.message : String(err);
485
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
486
- return { exitCode: 99, error };
487
- }
488
- if (result.outcome !== "completed") {
489
- const error = result.error ?? "agent did not complete";
490
- await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
491
- return { exitCode: 99, error };
465
+ const history = [];
466
+ let since = 0;
467
+ let lastActivityAt = now();
468
+ const startedAt = now();
469
+ let turnsProcessed = 0;
470
+ while (true) {
471
+ if (now() - startedAt > hardTimeoutMs) {
472
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
473
+ sessionId: opts.sessionId,
474
+ reason: "hard-timeout"
475
+ });
476
+ return { exitCode: 0, turnsProcessed, reason: "hard-timeout" };
477
+ }
478
+ let response;
479
+ try {
480
+ response = await opts.pull(since, pullTimeoutMs);
481
+ } catch (err) {
482
+ const msg = err instanceof Error ? err.message : String(err);
483
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: `pull failed: ${msg}` });
484
+ return { exitCode: 99, turnsProcessed, reason: `pull failed: ${msg}` };
485
+ }
486
+ if (response.turns.length === 0) {
487
+ if (now() - lastActivityAt > idleTimeoutMs) {
488
+ await emit(opts.sink, "chat.done", opts.sessionId, "done", {
489
+ sessionId: opts.sessionId,
490
+ reason: "idle-timeout"
491
+ });
492
+ return { exitCode: 0, turnsProcessed, reason: "idle-timeout" };
493
+ }
494
+ continue;
495
+ }
496
+ const newUserTurns = response.turns.filter((t) => t.role === "user");
497
+ for (const t of newUserTurns) history.push(t);
498
+ since = response.nextSince;
499
+ if (newUserTurns.length === 0) continue;
500
+ lastActivityAt = now();
501
+ const prompt = buildPrompt(history, systemPrompt);
502
+ let result;
503
+ try {
504
+ result = await invoke(prompt);
505
+ } catch (err) {
506
+ const msg = err instanceof Error ? err.message : String(err);
507
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error: msg });
508
+ return { exitCode: 99, turnsProcessed, reason: msg };
509
+ }
510
+ if (result.outcome !== "completed") {
511
+ const error = result.error ?? "agent did not complete";
512
+ await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
513
+ return { exitCode: 99, turnsProcessed, reason: error };
514
+ }
515
+ const reply = result.finalText.trim();
516
+ const replyTimestamp = (/* @__PURE__ */ new Date()).toISOString();
517
+ history.push({ role: "assistant", content: reply, timestamp: replyTimestamp });
518
+ turnsProcessed++;
519
+ lastActivityAt = now();
520
+ await emit(opts.sink, "chat.message", opts.sessionId, `message-${turnsProcessed}`, {
521
+ sessionId: opts.sessionId,
522
+ role: "assistant",
523
+ content: reply,
524
+ timestamp: replyTimestamp
525
+ });
492
526
  }
493
- const reply = result.finalText.trim();
494
- const now = (/* @__PURE__ */ new Date()).toISOString();
495
- appendTurn(opts.sessionFile, {
496
- role: "assistant",
497
- content: reply,
498
- timestamp: now
499
- });
500
- await emit(opts.sink, "chat.message", opts.sessionId, "message", {
501
- sessionId: opts.sessionId,
502
- role: "assistant",
503
- content: reply,
504
- timestamp: now
505
- });
506
- await emit(opts.sink, "chat.done", opts.sessionId, "done", { sessionId: opts.sessionId });
507
- return { exitCode: 0, reply };
508
527
  }
509
528
  function buildPrompt(turns, systemPrompt) {
510
529
  const header = `System: ${systemPrompt}`;
@@ -526,11 +545,11 @@ async function emit(sink, type, sessionId, suffix, payload) {
526
545
 
527
546
  // src/kody2-cli.ts
528
547
  import { execFileSync as execFileSync15 } from "child_process";
529
- import * as fs18 from "fs";
530
- import * as path15 from "path";
548
+ import * as fs16 from "fs";
549
+ import * as path13 from "path";
531
550
 
532
551
  // src/dispatch.ts
533
- import * as fs5 from "fs";
552
+ import * as fs3 from "fs";
534
553
  function autoDispatch(opts) {
535
554
  const explicit = opts?.explicit;
536
555
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
@@ -542,10 +561,10 @@ function autoDispatch(opts) {
542
561
  }
543
562
  const eventName = process.env.GITHUB_EVENT_NAME;
544
563
  const eventPath = process.env.GITHUB_EVENT_PATH;
545
- if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
564
+ if (!eventName || !eventPath || !fs3.existsSync(eventPath)) return null;
546
565
  let event = {};
547
566
  try {
548
- event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
567
+ event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
549
568
  } catch {
550
569
  return null;
551
570
  }
@@ -614,14 +633,14 @@ function extractFeedback(afterTag) {
614
633
  }
615
634
 
616
635
  // src/executor.ts
617
- import * as fs17 from "fs";
618
- import * as path14 from "path";
636
+ import * as fs15 from "fs";
637
+ import * as path12 from "path";
619
638
 
620
639
  // src/litellm.ts
621
640
  import { execFileSync, spawn } from "child_process";
622
- import * as fs6 from "fs";
641
+ import * as fs4 from "fs";
623
642
  import * as os from "os";
624
- import * as path5 from "path";
643
+ import * as path3 from "path";
625
644
  async function checkLitellmHealth(url) {
626
645
  try {
627
646
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -661,20 +680,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
661
680
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
662
681
  }
663
682
  }
664
- const configPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
665
- fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
683
+ const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
684
+ fs4.writeFileSync(configPath, generateLitellmConfigYaml(model));
666
685
  const portMatch = url.match(/:(\d+)/);
667
686
  const port = portMatch ? portMatch[1] : "4000";
668
687
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
669
688
  const dotenvVars = readDotenvApiKeys(projectDir);
670
- const logPath = path5.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
671
- const outFd = fs6.openSync(logPath, "w");
689
+ const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
690
+ const outFd = fs4.openSync(logPath, "w");
672
691
  const child = spawn(cmd, args, {
673
692
  stdio: ["ignore", outFd, outFd],
674
693
  detached: true,
675
694
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
676
695
  });
677
- fs6.closeSync(outFd);
696
+ fs4.closeSync(outFd);
678
697
  for (let i = 0; i < 30; i++) {
679
698
  await new Promise((r) => setTimeout(r, 2e3));
680
699
  if (await checkLitellmHealth(url)) {
@@ -691,7 +710,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
691
710
  }
692
711
  let logTail = "";
693
712
  try {
694
- logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
713
+ logTail = fs4.readFileSync(logPath, "utf-8").slice(-2e3);
695
714
  } catch {
696
715
  }
697
716
  try {
@@ -702,10 +721,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
702
721
  ${logTail}`);
703
722
  }
704
723
  function readDotenvApiKeys(projectDir) {
705
- const dotenvPath = path5.join(projectDir, ".env");
706
- if (!fs6.existsSync(dotenvPath)) return {};
724
+ const dotenvPath = path3.join(projectDir, ".env");
725
+ if (!fs4.existsSync(dotenvPath)) return {};
707
726
  const result = {};
708
- for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
727
+ for (const rawLine of fs4.readFileSync(dotenvPath, "utf-8").split("\n")) {
709
728
  const line = rawLine.trim();
710
729
  if (!line || line.startsWith("#")) continue;
711
730
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -728,8 +747,8 @@ function stripBlockingEnv(env) {
728
747
  }
729
748
 
730
749
  // src/profile.ts
731
- import * as fs7 from "fs";
732
- import * as path6 from "path";
750
+ import * as fs5 from "fs";
751
+ import * as path4 from "path";
733
752
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
734
753
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
735
754
  var ProfileError = class extends Error {
@@ -742,12 +761,12 @@ var ProfileError = class extends Error {
742
761
  profilePath;
743
762
  };
744
763
  function loadProfile(profilePath) {
745
- if (!fs7.existsSync(profilePath)) {
764
+ if (!fs5.existsSync(profilePath)) {
746
765
  throw new ProfileError(profilePath, "file not found");
747
766
  }
748
767
  let raw;
749
768
  try {
750
- raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
769
+ raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
751
770
  } catch (err) {
752
771
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
753
772
  }
@@ -771,7 +790,7 @@ function loadProfile(profilePath) {
771
790
  outputContract: r.outputContract,
772
791
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
773
792
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
774
- dir: path6.dirname(profilePath)
793
+ dir: path4.dirname(profilePath)
775
794
  };
776
795
  return profile;
777
796
  }
@@ -837,6 +856,7 @@ function parseClaudeCode(p, raw) {
837
856
  model: typeof r.model === "string" ? r.model : "inherit",
838
857
  permissionMode,
839
858
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
859
+ maxThinkingTokens: typeof r.maxThinkingTokens === "number" ? r.maxThinkingTokens : null,
840
860
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
841
861
  tools,
842
862
  hooks: Array.isArray(r.hooks) ? r.hooks : [],
@@ -950,21 +970,21 @@ function parseScriptList(p, key, raw) {
950
970
  }
951
971
 
952
972
  // src/scripts/buildSyntheticPlugin.ts
953
- import * as fs8 from "fs";
973
+ import * as fs6 from "fs";
954
974
  import * as os2 from "os";
955
- import * as path7 from "path";
975
+ import * as path5 from "path";
956
976
  function getPluginsCatalogRoot() {
957
- const here = path7.dirname(new URL(import.meta.url).pathname);
977
+ const here = path5.dirname(new URL(import.meta.url).pathname);
958
978
  const candidates = [
959
- path7.join(here, "..", "plugins"),
979
+ path5.join(here, "..", "plugins"),
960
980
  // dev: src/scripts → src/plugins
961
- path7.join(here, "..", "..", "plugins"),
981
+ path5.join(here, "..", "..", "plugins"),
962
982
  // built: dist/scripts → dist/plugins
963
- path7.join(here, "..", "..", "src", "plugins")
983
+ path5.join(here, "..", "..", "src", "plugins")
964
984
  // fallback
965
985
  ];
966
986
  for (const c of candidates) {
967
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
987
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
968
988
  }
969
989
  return candidates[0];
970
990
  }
@@ -974,50 +994,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
974
994
  if (!needsSynthetic) return;
975
995
  const catalog = getPluginsCatalogRoot();
976
996
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
977
- const root = path7.join(os2.tmpdir(), `kody2-synth-${runId}`);
978
- fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
997
+ const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
998
+ fs6.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
979
999
  if (cc.skills.length > 0) {
980
- const dst = path7.join(root, "skills");
981
- fs8.mkdirSync(dst, { recursive: true });
1000
+ const dst = path5.join(root, "skills");
1001
+ fs6.mkdirSync(dst, { recursive: true });
982
1002
  for (const name of cc.skills) {
983
- const src = path7.join(catalog, "skills", name);
984
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
985
- copyDir(src, path7.join(dst, name));
1003
+ const src = path5.join(catalog, "skills", name);
1004
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1005
+ copyDir(src, path5.join(dst, name));
986
1006
  }
987
1007
  }
988
1008
  if (cc.commands.length > 0) {
989
- const dst = path7.join(root, "commands");
990
- fs8.mkdirSync(dst, { recursive: true });
1009
+ const dst = path5.join(root, "commands");
1010
+ fs6.mkdirSync(dst, { recursive: true });
991
1011
  for (const name of cc.commands) {
992
- const src = path7.join(catalog, "commands", `${name}.md`);
993
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
994
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1012
+ const src = path5.join(catalog, "commands", `${name}.md`);
1013
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1014
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
995
1015
  }
996
1016
  }
997
1017
  if (cc.subagents.length > 0) {
998
- const dst = path7.join(root, "agents");
999
- fs8.mkdirSync(dst, { recursive: true });
1018
+ const dst = path5.join(root, "agents");
1019
+ fs6.mkdirSync(dst, { recursive: true });
1000
1020
  for (const name of cc.subagents) {
1001
- const src = path7.join(catalog, "agents", `${name}.md`);
1002
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1003
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1021
+ const src = path5.join(catalog, "agents", `${name}.md`);
1022
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1023
+ fs6.copyFileSync(src, path5.join(dst, `${name}.md`));
1004
1024
  }
1005
1025
  }
1006
1026
  if (cc.hooks.length > 0) {
1007
- const dst = path7.join(root, "hooks");
1008
- fs8.mkdirSync(dst, { recursive: true });
1027
+ const dst = path5.join(root, "hooks");
1028
+ fs6.mkdirSync(dst, { recursive: true });
1009
1029
  const merged = { hooks: {} };
1010
1030
  for (const name of cc.hooks) {
1011
- const src = path7.join(catalog, "hooks", `${name}.json`);
1012
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1013
- const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
1031
+ const src = path5.join(catalog, "hooks", `${name}.json`);
1032
+ if (!fs6.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1033
+ const parsed = JSON.parse(fs6.readFileSync(src, "utf-8"));
1014
1034
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1015
1035
  if (!Array.isArray(entries)) continue;
1016
1036
  if (!merged.hooks[event]) merged.hooks[event] = [];
1017
1037
  merged.hooks[event].push(...entries);
1018
1038
  }
1019
1039
  }
1020
- fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1040
+ fs6.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1021
1041
  `);
1022
1042
  }
1023
1043
  const manifest = {
@@ -1028,17 +1048,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1028
1048
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1029
1049
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1030
1050
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1031
- fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1051
+ fs6.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1032
1052
  `);
1033
1053
  ctx.data.syntheticPluginPath = root;
1034
1054
  };
1035
1055
  function copyDir(src, dst) {
1036
- fs8.mkdirSync(dst, { recursive: true });
1037
- for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
1038
- const s = path7.join(src, ent.name);
1039
- const d = path7.join(dst, ent.name);
1056
+ fs6.mkdirSync(dst, { recursive: true });
1057
+ for (const ent of fs6.readdirSync(src, { withFileTypes: true })) {
1058
+ const s = path5.join(src, ent.name);
1059
+ const d = path5.join(dst, ent.name);
1040
1060
  if (ent.isDirectory()) copyDir(s, d);
1041
- else if (ent.isFile()) fs8.copyFileSync(s, d);
1061
+ else if (ent.isFile()) fs6.copyFileSync(s, d);
1042
1062
  }
1043
1063
  }
1044
1064
 
@@ -1104,18 +1124,18 @@ function formatMissesForFeedback(misses) {
1104
1124
  }
1105
1125
 
1106
1126
  // src/prompt.ts
1107
- import * as fs9 from "fs";
1108
- import * as path8 from "path";
1127
+ import * as fs7 from "fs";
1128
+ import * as path6 from "path";
1109
1129
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1110
1130
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1111
1131
  function loadProjectConventions(projectDir) {
1112
1132
  const out = [];
1113
1133
  for (const rel of CONVENTION_FILES) {
1114
- const abs = path8.join(projectDir, rel);
1115
- if (!fs9.existsSync(abs)) continue;
1134
+ const abs = path6.join(projectDir, rel);
1135
+ if (!fs7.existsSync(abs)) continue;
1116
1136
  let content;
1117
1137
  try {
1118
- content = fs9.readFileSync(abs, "utf-8");
1138
+ content = fs7.readFileSync(abs, "utf-8");
1119
1139
  } catch {
1120
1140
  continue;
1121
1141
  }
@@ -1236,8 +1256,8 @@ import { execFileSync as execFileSync4 } from "child_process";
1236
1256
 
1237
1257
  // src/commit.ts
1238
1258
  import { execFileSync as execFileSync3 } from "child_process";
1239
- import * as fs10 from "fs";
1240
- import * as path9 from "path";
1259
+ import * as fs8 from "fs";
1260
+ import * as path7 from "path";
1241
1261
  var FORBIDDEN_PATH_PREFIXES = [
1242
1262
  ".kody/",
1243
1263
  ".kody-engine/",
@@ -1292,18 +1312,18 @@ function tryGit(args, cwd) {
1292
1312
  }
1293
1313
  function abortUnfinishedGitOps(cwd) {
1294
1314
  const aborted = [];
1295
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1296
- if (!fs10.existsSync(gitDir)) return aborted;
1297
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1315
+ const gitDir = path7.join(cwd ?? process.cwd(), ".git");
1316
+ if (!fs8.existsSync(gitDir)) return aborted;
1317
+ if (fs8.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
1298
1318
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1299
1319
  }
1300
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1320
+ if (fs8.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
1301
1321
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1302
1322
  }
1303
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1323
+ if (fs8.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
1304
1324
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1305
1325
  }
1306
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1326
+ if (fs8.existsSync(path7.join(gitDir, "rebase-merge")) || fs8.existsSync(path7.join(gitDir, "rebase-apply"))) {
1307
1327
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1308
1328
  }
1309
1329
  try {
@@ -1345,7 +1365,7 @@ function normalizeCommitMessage(raw) {
1345
1365
  function commitAndPush(branch, agentMessage, cwd) {
1346
1366
  const allChanged = listChangedFiles(cwd);
1347
1367
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1348
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1368
+ const mergeHeadExists = fs8.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1349
1369
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1350
1370
  return { committed: false, pushed: false, sha: "", message: "" };
1351
1371
  }
@@ -1394,6 +1414,11 @@ var commitAndPush2 = async (ctx, profile) => {
1394
1414
  ctx.data.commitResult = { committed: false, pushed: false };
1395
1415
  return;
1396
1416
  }
1417
+ if (ctx.data.agentDone === false) {
1418
+ ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
1419
+ ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
1420
+ return;
1421
+ }
1397
1422
  const kind = profile.name;
1398
1423
  if (kind === "resolve") {
1399
1424
  try {
@@ -1435,20 +1460,20 @@ function defaultCommitMessage(mode, data) {
1435
1460
  }
1436
1461
 
1437
1462
  // src/scripts/composePrompt.ts
1438
- import * as fs11 from "fs";
1439
- import * as path10 from "path";
1463
+ import * as fs9 from "fs";
1464
+ import * as path8 from "path";
1440
1465
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1441
1466
  var composePrompt = async (ctx, profile) => {
1442
1467
  const explicit = ctx.data.promptTemplate;
1443
1468
  const mode = ctx.args.mode;
1444
1469
  const candidates = [
1445
- explicit ? path10.join(profile.dir, explicit) : null,
1446
- mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1447
- path10.join(profile.dir, "prompt.md")
1470
+ explicit ? path8.join(profile.dir, explicit) : null,
1471
+ mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1472
+ path8.join(profile.dir, "prompt.md")
1448
1473
  ].filter(Boolean);
1449
1474
  let templatePath = "";
1450
1475
  for (const c of candidates) {
1451
- if (fs11.existsSync(c)) {
1476
+ if (fs9.existsSync(c)) {
1452
1477
  templatePath = c;
1453
1478
  break;
1454
1479
  }
@@ -1456,7 +1481,7 @@ var composePrompt = async (ctx, profile) => {
1456
1481
  if (!templatePath) {
1457
1482
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1458
1483
  }
1459
- const template = fs11.readFileSync(templatePath, "utf-8");
1484
+ const template = fs9.readFileSync(templatePath, "utf-8");
1460
1485
  const tokens = {
1461
1486
  ...stringifyAll(ctx.args, "args."),
1462
1487
  ...stringifyAll(ctx.data, ""),
@@ -1931,7 +1956,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1931
1956
 
1932
1957
  // src/gha.ts
1933
1958
  import { execFileSync as execFileSync7 } from "child_process";
1934
- import * as fs12 from "fs";
1959
+ import * as fs10 from "fs";
1935
1960
  function getRunUrl() {
1936
1961
  const server = process.env.GITHUB_SERVER_URL;
1937
1962
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1942,10 +1967,10 @@ function getRunUrl() {
1942
1967
  function reactToTriggerComment(cwd) {
1943
1968
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1944
1969
  const eventPath = process.env.GITHUB_EVENT_PATH;
1945
- if (!eventPath || !fs12.existsSync(eventPath)) return;
1970
+ if (!eventPath || !fs10.existsSync(eventPath)) return;
1946
1971
  let event = null;
1947
1972
  try {
1948
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
1973
+ event = JSON.parse(fs10.readFileSync(eventPath, "utf-8"));
1949
1974
  } catch {
1950
1975
  return;
1951
1976
  }
@@ -2171,35 +2196,35 @@ function tryPostPr2(prNumber, body, cwd) {
2171
2196
 
2172
2197
  // src/scripts/initFlow.ts
2173
2198
  import { execFileSync as execFileSync9 } from "child_process";
2174
- import * as fs14 from "fs";
2175
- import * as path12 from "path";
2199
+ import * as fs12 from "fs";
2200
+ import * as path10 from "path";
2176
2201
 
2177
2202
  // src/registry.ts
2178
- import * as fs13 from "fs";
2179
- import * as path11 from "path";
2203
+ import * as fs11 from "fs";
2204
+ import * as path9 from "path";
2180
2205
  function getExecutablesRoot() {
2181
- const here = path11.dirname(new URL(import.meta.url).pathname);
2206
+ const here = path9.dirname(new URL(import.meta.url).pathname);
2182
2207
  const candidates = [
2183
- path11.join(here, "executables"),
2208
+ path9.join(here, "executables"),
2184
2209
  // dev: src/
2185
- path11.join(here, "..", "executables"),
2210
+ path9.join(here, "..", "executables"),
2186
2211
  // built: dist/bin → dist/executables
2187
- path11.join(here, "..", "src", "executables")
2212
+ path9.join(here, "..", "src", "executables")
2188
2213
  // fallback
2189
2214
  ];
2190
2215
  for (const c of candidates) {
2191
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2216
+ if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2192
2217
  }
2193
2218
  return candidates[0];
2194
2219
  }
2195
2220
  function listExecutables(root = getExecutablesRoot()) {
2196
- if (!fs13.existsSync(root)) return [];
2197
- const entries = fs13.readdirSync(root, { withFileTypes: true });
2221
+ if (!fs11.existsSync(root)) return [];
2222
+ const entries = fs11.readdirSync(root, { withFileTypes: true });
2198
2223
  const out = [];
2199
2224
  for (const ent of entries) {
2200
2225
  if (!ent.isDirectory()) continue;
2201
- const profilePath = path11.join(root, ent.name, "profile.json");
2202
- if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
2226
+ const profilePath = path9.join(root, ent.name, "profile.json");
2227
+ if (fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile()) {
2203
2228
  out.push({ name: ent.name, profilePath });
2204
2229
  }
2205
2230
  }
@@ -2207,8 +2232,8 @@ function listExecutables(root = getExecutablesRoot()) {
2207
2232
  }
2208
2233
  function hasExecutable(name, root = getExecutablesRoot()) {
2209
2234
  if (!isSafeName(name)) return false;
2210
- const profilePath = path11.join(root, name, "profile.json");
2211
- return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
2235
+ const profilePath = path9.join(root, name, "profile.json");
2236
+ return fs11.existsSync(profilePath) && fs11.statSync(profilePath).isFile();
2212
2237
  }
2213
2238
  function isSafeName(name) {
2214
2239
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -2237,9 +2262,9 @@ function parseGenericFlags(argv) {
2237
2262
 
2238
2263
  // src/scripts/initFlow.ts
2239
2264
  function detectPackageManager(cwd) {
2240
- if (fs14.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2241
- if (fs14.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
2242
- if (fs14.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
2265
+ if (fs12.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2266
+ if (fs12.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2267
+ if (fs12.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2243
2268
  return "npm";
2244
2269
  }
2245
2270
  function qualityCommandsFor(pm) {
@@ -2360,22 +2385,22 @@ function performInit(cwd, force) {
2360
2385
  const pm = detectPackageManager(cwd);
2361
2386
  const ownerRepo = detectOwnerRepo(cwd);
2362
2387
  const defaultBranch = defaultBranchFromGit(cwd);
2363
- const configPath = path12.join(cwd, "kody.config.json");
2364
- if (fs14.existsSync(configPath) && !force) {
2388
+ const configPath = path10.join(cwd, "kody.config.json");
2389
+ if (fs12.existsSync(configPath) && !force) {
2365
2390
  skipped.push("kody.config.json");
2366
2391
  } else {
2367
2392
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
2368
- fs14.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2393
+ fs12.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
2369
2394
  `);
2370
2395
  wrote.push("kody.config.json");
2371
2396
  }
2372
- const workflowDir = path12.join(cwd, ".github", "workflows");
2373
- const workflowPath = path12.join(workflowDir, "kody2.yml");
2374
- if (fs14.existsSync(workflowPath) && !force) {
2397
+ const workflowDir = path10.join(cwd, ".github", "workflows");
2398
+ const workflowPath = path10.join(workflowDir, "kody2.yml");
2399
+ if (fs12.existsSync(workflowPath) && !force) {
2375
2400
  skipped.push(".github/workflows/kody2.yml");
2376
2401
  } else {
2377
- fs14.mkdirSync(workflowDir, { recursive: true });
2378
- fs14.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2402
+ fs12.mkdirSync(workflowDir, { recursive: true });
2403
+ fs12.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
2379
2404
  wrote.push(".github/workflows/kody2.yml");
2380
2405
  }
2381
2406
  for (const exe of listExecutables()) {
@@ -2386,12 +2411,12 @@ function performInit(cwd, force) {
2386
2411
  continue;
2387
2412
  }
2388
2413
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
2389
- const target = path12.join(workflowDir, `kody2-${exe.name}.yml`);
2390
- if (fs14.existsSync(target) && !force) {
2414
+ const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
2415
+ if (fs12.existsSync(target) && !force) {
2391
2416
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
2392
2417
  continue;
2393
2418
  }
2394
- fs14.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2419
+ fs12.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
2395
2420
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
2396
2421
  }
2397
2422
  return { wrote, skipped };
@@ -2898,8 +2923,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2898
2923
 
2899
2924
  // src/scripts/releaseFlow.ts
2900
2925
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2901
- import * as fs15 from "fs";
2902
- import * as path13 from "path";
2926
+ import * as fs13 from "fs";
2927
+ import * as path11 from "path";
2903
2928
  function bumpVersion(current, bump) {
2904
2929
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2905
2930
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2915,12 +2940,12 @@ function bumpVersion(current, bump) {
2915
2940
  return `${major}.${minor}.${patch}`;
2916
2941
  }
2917
2942
  function updateVersionInFile(file, newVersion, cwd) {
2918
- const abs = path13.join(cwd, file);
2919
- if (!fs15.existsSync(abs)) return false;
2920
- const content = fs15.readFileSync(abs, "utf-8");
2943
+ const abs = path11.join(cwd, file);
2944
+ if (!fs13.existsSync(abs)) return false;
2945
+ const content = fs13.readFileSync(abs, "utf-8");
2921
2946
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2922
2947
  if (updated === content) return false;
2923
- fs15.writeFileSync(abs, updated);
2948
+ fs13.writeFileSync(abs, updated);
2924
2949
  return true;
2925
2950
  }
2926
2951
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2968,19 +2993,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2968
2993
  return parts.join("\n");
2969
2994
  }
2970
2995
  function prependChangelog(cwd, entry) {
2971
- const p = path13.join(cwd, "CHANGELOG.md");
2996
+ const p = path11.join(cwd, "CHANGELOG.md");
2972
2997
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2973
- if (fs15.existsSync(p)) {
2974
- const prior = fs15.readFileSync(p, "utf-8");
2998
+ if (fs13.existsSync(p)) {
2999
+ const prior = fs13.readFileSync(p, "utf-8");
2975
3000
  if (/^#\s*Changelog\b/m.test(prior)) {
2976
3001
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2977
- fs15.writeFileSync(p, `${prior.slice(0, idx + 1)}
3002
+ fs13.writeFileSync(p, `${prior.slice(0, idx + 1)}
2978
3003
  ${entry}${prior.slice(idx + 1)}`);
2979
3004
  } else {
2980
- fs15.writeFileSync(p, `${header}${entry}${prior}`);
3005
+ fs13.writeFileSync(p, `${header}${entry}${prior}`);
2981
3006
  }
2982
3007
  } else {
2983
- fs15.writeFileSync(p, `${header}${entry}`);
3008
+ fs13.writeFileSync(p, `${header}${entry}`);
2984
3009
  }
2985
3010
  }
2986
3011
  function git3(args, cwd, timeout = 6e4) {
@@ -3031,13 +3056,13 @@ var releaseFlow = async (ctx) => {
3031
3056
  };
3032
3057
  async function runPrepare(args) {
3033
3058
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
3034
- const pkgPath = path13.join(cwd, "package.json");
3035
- if (!fs15.existsSync(pkgPath)) {
3059
+ const pkgPath = path11.join(cwd, "package.json");
3060
+ if (!fs13.existsSync(pkgPath)) {
3036
3061
  ctx.output.exitCode = 99;
3037
3062
  ctx.output.reason = "release prepare: package.json not found";
3038
3063
  return;
3039
3064
  }
3040
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3065
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3041
3066
  if (typeof pkg.version !== "string") {
3042
3067
  ctx.output.exitCode = 99;
3043
3068
  ctx.output.reason = "release prepare: package.json has no version";
@@ -3108,8 +3133,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
3108
3133
  }
3109
3134
  async function runFinalize(args) {
3110
3135
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
3111
- const pkgPath = path13.join(cwd, "package.json");
3112
- const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
3136
+ const pkgPath = path11.join(cwd, "package.json");
3137
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3113
3138
  if (typeof pkg.version !== "string") {
3114
3139
  ctx.output.exitCode = 99;
3115
3140
  ctx.output.reason = "release finalize: package.json has no version";
@@ -3787,7 +3812,7 @@ var watchStalePrsFlow = async (ctx) => {
3787
3812
  };
3788
3813
 
3789
3814
  // src/scripts/writeRunSummary.ts
3790
- import * as fs16 from "fs";
3815
+ import * as fs14 from "fs";
3791
3816
  var writeRunSummary = async (ctx, profile) => {
3792
3817
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
3793
3818
  if (!summaryPath) return;
@@ -3809,7 +3834,7 @@ var writeRunSummary = async (ctx, profile) => {
3809
3834
  if (reason) lines.push(`- **Reason:** ${reason}`);
3810
3835
  lines.push("");
3811
3836
  try {
3812
- fs16.appendFileSync(summaryPath, `${lines.join("\n")}
3837
+ fs14.appendFileSync(summaryPath, `${lines.join("\n")}
3813
3838
  `);
3814
3839
  } catch {
3815
3840
  }
@@ -3957,9 +3982,9 @@ async function runExecutable(profileName, input) {
3957
3982
  data: {},
3958
3983
  output: { exitCode: 0 }
3959
3984
  };
3960
- const ndjsonDir = path14.join(input.cwd, ".kody2");
3985
+ const ndjsonDir = path12.join(input.cwd, ".kody2");
3961
3986
  const invokeAgent = async (prompt) => {
3962
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path14.isAbsolute(p) ? p : path14.resolve(profile.dir, p)).filter((p) => p.length > 0);
3987
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
3963
3988
  const syntheticPath = ctx.data.syntheticPluginPath;
3964
3989
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
3965
3990
  return runAgent({
@@ -3975,6 +4000,7 @@ async function runExecutable(profileName, input) {
3975
4000
  mcpServers: profile.claudeCode.mcpServers,
3976
4001
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
3977
4002
  maxTurns: profile.claudeCode.maxTurns,
4003
+ maxThinkingTokens: profile.claudeCode.maxThinkingTokens,
3978
4004
  systemPromptAppend: profile.claudeCode.systemPromptAppend,
3979
4005
  settingSources: profile.claudeCode.settingSources
3980
4006
  });
@@ -4025,17 +4051,17 @@ async function runExecutable(profileName, input) {
4025
4051
  }
4026
4052
  }
4027
4053
  function resolveProfilePath(profileName) {
4028
- const here = path14.dirname(new URL(import.meta.url).pathname);
4054
+ const here = path12.dirname(new URL(import.meta.url).pathname);
4029
4055
  const candidates = [
4030
- path14.join(here, "executables", profileName, "profile.json"),
4056
+ path12.join(here, "executables", profileName, "profile.json"),
4031
4057
  // same-dir sibling (dev)
4032
- path14.join(here, "..", "executables", profileName, "profile.json"),
4058
+ path12.join(here, "..", "executables", profileName, "profile.json"),
4033
4059
  // up one (prod: dist/bin → dist/executables)
4034
- path14.join(here, "..", "src", "executables", profileName, "profile.json")
4060
+ path12.join(here, "..", "src", "executables", profileName, "profile.json")
4035
4061
  // fallback
4036
4062
  ];
4037
4063
  for (const c of candidates) {
4038
- if (fs17.existsSync(c)) return c;
4064
+ if (fs15.existsSync(c)) return c;
4039
4065
  }
4040
4066
  return candidates[0];
4041
4067
  }
@@ -4211,9 +4237,9 @@ function resolveAuthToken(env = process.env) {
4211
4237
  return token;
4212
4238
  }
4213
4239
  function detectPackageManager2(cwd) {
4214
- if (fs18.existsSync(path15.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4215
- if (fs18.existsSync(path15.join(cwd, "yarn.lock"))) return "yarn";
4216
- if (fs18.existsSync(path15.join(cwd, "bun.lockb"))) return "bun";
4240
+ if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4241
+ if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
4242
+ if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
4217
4243
  return "npm";
4218
4244
  }
4219
4245
  function shellOut(cmd, args, cwd, stream = true) {
@@ -4293,11 +4319,11 @@ function configureGitIdentity(cwd) {
4293
4319
  }
4294
4320
  function postFailureTail(issueNumber, cwd, reason) {
4295
4321
  if (!issueNumber) return;
4296
- const logPath = path15.join(cwd, ".kody2", "last-run.jsonl");
4322
+ const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
4297
4323
  let tail = "";
4298
4324
  try {
4299
- if (fs18.existsSync(logPath)) {
4300
- const content = fs18.readFileSync(logPath, "utf-8");
4325
+ if (fs16.existsSync(logPath)) {
4326
+ const content = fs16.readFileSync(logPath, "utf-8");
4301
4327
  tail = content.slice(-3e3);
4302
4328
  }
4303
4329
  } catch {
@@ -4322,7 +4348,7 @@ async function runCi(argv) {
4322
4348
  return 0;
4323
4349
  }
4324
4350
  const args = parseCiArgs(argv);
4325
- const cwd = args.cwd ? path15.resolve(args.cwd) : process.cwd();
4351
+ const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
4326
4352
  let earlyConfig;
4327
4353
  try {
4328
4354
  earlyConfig = loadConfig(cwd);
@@ -4415,23 +4441,23 @@ var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
4415
4441
  var CHAT_HELP = `kody2 chat \u2014 dashboard-driven chat session
4416
4442
 
4417
4443
  Usage:
4418
- kody2 chat [--session <id>] [--message <text>] [--model <provider/model>]
4444
+ kody2 chat [--session <id>] [--model <provider/model>]
4419
4445
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
4420
4446
 
4421
- All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
4422
- CLI flags take precedence over env. SESSION_ID is required.
4447
+ All inputs may also come from env: SESSION_ID, MODEL, DASHBOARD_URL.
4448
+ CLI flags take precedence over env. SESSION_ID and DASHBOARD_URL are required
4449
+ (the runner long-polls the dashboard for user turns and pushes events back).
4423
4450
 
4424
4451
  Exit codes:
4425
- 0 reply emitted successfully
4426
- 64 bad inputs (missing session, empty history)
4427
- 99 runtime failure (agent crash, LiteLLM failure)
4452
+ 0 session exited cleanly (idle or hard timeout)
4453
+ 64 bad inputs
4454
+ 99 runtime failure (agent crash, pull failure, LiteLLM failure)
4428
4455
  `;
4429
4456
  function parseChatArgs(argv, env = process.env) {
4430
4457
  const result = { errors: [] };
4431
4458
  for (let i = 0; i < argv.length; i++) {
4432
4459
  const arg = argv[i];
4433
4460
  if (arg === "--session") result.sessionId = argv[++i];
4434
- else if (arg === "--message") result.initMessage = argv[++i];
4435
4461
  else if (arg === "--model") result.model = argv[++i];
4436
4462
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
4437
4463
  else if (arg === "--cwd") result.cwd = argv[++i];
@@ -4442,34 +4468,18 @@ function parseChatArgs(argv, env = process.env) {
4442
4468
  else if (arg) result.errors.push(`unexpected positional: ${arg}`);
4443
4469
  }
4444
4470
  result.sessionId = result.sessionId ?? env.SESSION_ID ?? void 0;
4445
- result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
4446
4471
  result.model = result.model ?? env.MODEL ?? void 0;
4447
4472
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
4448
- for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
4473
+ for (const key of ["sessionId", "model", "dashboardUrl"]) {
4449
4474
  const v = result[key];
4450
4475
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
4451
4476
  }
4452
- if (!result.sessionId && !result.errors.includes("__HELP__")) {
4453
- result.errors.push("--session <id> (or SESSION_ID env) is required");
4477
+ if (!result.errors.includes("__HELP__")) {
4478
+ if (!result.sessionId) result.errors.push("--session <id> (or SESSION_ID env) is required");
4479
+ if (!result.dashboardUrl) result.errors.push("--dashboard-url <url> (or DASHBOARD_URL env) is required");
4454
4480
  }
4455
4481
  return result;
4456
4482
  }
4457
- function commitChatFiles(cwd, sessionId, verbose) {
4458
- const sessionFile = path16.relative(cwd, sessionFilePath(cwd, sessionId));
4459
- const eventsFile = path16.relative(cwd, eventsFilePath(cwd, sessionId));
4460
- const paths = [sessionFile, eventsFile].filter((p) => fs19.existsSync(path16.join(cwd, p)));
4461
- if (paths.length === 0) return;
4462
- const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
4463
- try {
4464
- execFileSync16("git", ["add", ...paths], opts);
4465
- execFileSync16("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
4466
- execFileSync16("git", ["push", "--quiet", "origin", "HEAD"], opts);
4467
- } catch (err) {
4468
- const msg = err instanceof Error ? err.message : String(err);
4469
- process.stderr.write(`[kody2:chat] commit/push skipped: ${msg}
4470
- `);
4471
- }
4472
- }
4473
4483
  function tryLoadConfig(cwd) {
4474
4484
  try {
4475
4485
  return loadConfig(cwd);
@@ -4477,11 +4487,6 @@ function tryLoadConfig(cwd) {
4477
4487
  return null;
4478
4488
  }
4479
4489
  }
4480
- function buildSink(cwd, sessionId, dashboardUrl) {
4481
- const sinks = [new FileSink(eventsFilePath(cwd, sessionId))];
4482
- if (dashboardUrl) sinks.push(new HttpSink(dashboardUrl, sessionId));
4483
- return new TeeSink(sinks);
4484
- }
4485
4490
  async function runChat(argv) {
4486
4491
  if (argv.includes("--help") || argv.includes("-h")) {
4487
4492
  process.stdout.write(CHAT_HELP);
@@ -4495,8 +4500,9 @@ async function runChat(argv) {
4495
4500
  ${CHAT_HELP}`);
4496
4501
  return 64;
4497
4502
  }
4498
- const cwd = args.cwd ? path16.resolve(args.cwd) : process.cwd();
4503
+ const cwd = args.cwd ? path14.resolve(args.cwd) : process.cwd();
4499
4504
  const sessionId = args.sessionId;
4505
+ const dashboardUrl = args.dashboardUrl;
4500
4506
  const unpackedSecrets = unpackAllSecrets();
4501
4507
  if (unpackedSecrets > 0) {
4502
4508
  process.stdout.write(`\u2192 kody2: unpacked ${unpackedSecrets} secret(s) from ALL_SECRETS
@@ -4522,13 +4528,20 @@ ${CHAT_HELP}`);
4522
4528
  return 99;
4523
4529
  }
4524
4530
  }
4531
+ let sink;
4532
+ try {
4533
+ sink = new HttpSink(dashboardUrl, sessionId);
4534
+ } catch (err) {
4535
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4536
+ `);
4537
+ return 64;
4538
+ }
4525
4539
  let litellm = null;
4526
4540
  try {
4527
4541
  litellm = await startLitellmIfNeeded(model, cwd);
4528
4542
  } catch (err) {
4529
4543
  const msg = err instanceof Error ? err.message : String(err);
4530
- const sink2 = buildSink(cwd, sessionId, args.dashboardUrl);
4531
- await sink2.emit({
4544
+ await sink.emit({
4532
4545
  event: "chat.error",
4533
4546
  payload: { sessionId, error: `litellm startup failed: ${msg}` },
4534
4547
  runId: makeRunId(sessionId, "error"),
@@ -4536,21 +4549,33 @@ ${CHAT_HELP}`);
4536
4549
  });
4537
4550
  return 99;
4538
4551
  }
4539
- const sessionFile = sessionFilePath(cwd, sessionId);
4540
- if (args.initMessage) seedInitialMessage(sessionFile, args.initMessage);
4541
- const sink = buildSink(cwd, sessionId, args.dashboardUrl);
4552
+ let pull;
4553
+ try {
4554
+ pull = createPullClient({ baseUrl: dashboardUrl, sessionId });
4555
+ } catch (err) {
4556
+ process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
4557
+ `);
4558
+ try {
4559
+ litellm?.kill();
4560
+ } catch {
4561
+ }
4562
+ return 64;
4563
+ }
4564
+ process.stdout.write(`\u2192 kody2 chat: session ${sessionId}, model ${model.provider}/${model.model}
4565
+ `);
4542
4566
  try {
4543
- const result = await runChatTurn({
4567
+ const result = await runChatSession({
4544
4568
  sessionId,
4545
- sessionFile,
4546
4569
  cwd,
4547
4570
  model,
4548
4571
  litellmUrl: litellm?.url ?? null,
4549
4572
  sink,
4573
+ pull,
4550
4574
  verbose: args.verbose,
4551
4575
  quiet: args.quiet
4552
4576
  });
4553
- commitChatFiles(cwd, sessionId, args.verbose ?? false);
4577
+ process.stdout.write(`\u2192 kody2 chat: exited (${result.reason ?? "ok"}) after ${result.turnsProcessed} turn(s)
4578
+ `);
4554
4579
  return result.exitCode;
4555
4580
  } finally {
4556
4581
  try {
@@ -87,6 +87,8 @@ export interface ClaudeCodeSpec {
87
87
  permissionMode: "default" | "acceptEdits" | "plan" | "bypassPermissions"
88
88
  /** null = unbounded. */
89
89
  maxTurns: number | null
90
+ /** Extended-thinking token budget. null = SDK default. */
91
+ maxThinkingTokens: number | null
90
92
  /** Text appended on top of Claude Code's baseline system prompt. */
91
93
  systemPromptAppend: string | null
92
94
  /** SDK built-in tools this executable is allowed to use (capability pack). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,18 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody2": "tsx bin/kody2.ts",
17
+ "build": "tsup && node scripts/copy-assets.cjs",
18
+ "test": "vitest run tests/unit tests/int --no-coverage",
19
+ "test:e2e": "vitest run tests/e2e --no-coverage",
20
+ "test:all": "vitest run tests --no-coverage",
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "biome check",
23
+ "lint:fix": "biome check --write",
24
+ "format": "biome format --write",
25
+ "prepublishOnly": "pnpm build"
26
+ },
15
27
  "dependencies": {
16
28
  "@anthropic-ai/claude-agent-sdk": "0.2.92"
17
29
  },
@@ -31,16 +43,5 @@
31
43
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
32
44
  },
33
45
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
34
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
35
- "scripts": {
36
- "kody2": "tsx bin/kody2.ts",
37
- "build": "tsup && node scripts/copy-assets.cjs",
38
- "test": "vitest run tests/unit tests/int --no-coverage",
39
- "test:e2e": "vitest run tests/e2e --no-coverage",
40
- "test:all": "vitest run tests --no-coverage",
41
- "typecheck": "tsc --noEmit",
42
- "lint": "biome check",
43
- "lint:fix": "biome check --write",
44
- "format": "biome format --write"
45
- }
46
- }
46
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
47
+ }