@kody-ade/kody-engine 0.4.222 → 0.4.223

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/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.222",
18
+ version: "0.4.223",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -146,6 +146,12 @@ var init_claudeBinary = __esm({
146
146
  // src/config.ts
147
147
  import * as fs2 from "fs";
148
148
  import * as path2 from "path";
149
+ function parseReasoningEffort(raw) {
150
+ if (!raw) return null;
151
+ const v = raw.trim().toLowerCase();
152
+ if (REASONING_EFFORTS.includes(v)) return v;
153
+ return null;
154
+ }
149
155
  function parseProviderModel(s) {
150
156
  const slash = s.indexOf("/");
151
157
  if (slash <= 0 || slash === s.length - 1) {
@@ -198,7 +204,8 @@ function loadConfig(projectDir = process.cwd()) {
198
204
  },
199
205
  agent: {
200
206
  model: String(agent.model),
201
- ...parsePerExecutable(agent.perExecutable)
207
+ ...parsePerExecutable(agent.perExecutable),
208
+ ...parseAgentReasoningEffort(agent.reasoningEffort)
202
209
  },
203
210
  issueContext: parseIssueContext(raw.issueContext),
204
211
  testRequirements: parseTestRequirements(raw.testRequirements),
@@ -275,6 +282,10 @@ function parsePerExecutable(raw) {
275
282
  }
276
283
  return Object.keys(out).length > 0 ? { perExecutable: out } : {};
277
284
  }
285
+ function parseAgentReasoningEffort(raw) {
286
+ if (typeof raw !== "string") return {};
287
+ return { reasoningEffort: parseReasoningEffort(raw) ?? void 0 };
288
+ }
278
289
  function parseClassifyConfig(raw) {
279
290
  if (!raw || typeof raw !== "object") return void 0;
280
291
  const r = raw;
@@ -327,10 +338,16 @@ function parseTestRequirements(raw) {
327
338
  function getAnthropicApiKeyOrDummy() {
328
339
  return process.env.ANTHROPIC_API_KEY || `sk-ant-api03-${"0".repeat(64)}`;
329
340
  }
330
- var LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
341
+ var REASONING_EFFORTS, REASONING_BUDGETS, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
331
342
  var init_config = __esm({
332
343
  "src/config.ts"() {
333
344
  "use strict";
345
+ REASONING_EFFORTS = ["off", "low", "medium", "high"];
346
+ REASONING_BUDGETS = {
347
+ low: 2048,
348
+ medium: 1e4,
349
+ high: 32e3
350
+ };
334
351
  LITELLM_DEFAULT_PORT = 4e3;
335
352
  LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
336
353
  GITHUB_AUTHOR_ASSOCIATIONS = [
@@ -1664,15 +1681,19 @@ async function runAgent(opts) {
1664
1681
  let finalText = "";
1665
1682
  let getSubmitted;
1666
1683
  for (let attempt = 0; ; attempt++) {
1684
+ let ndjsonWriteFailed = false;
1685
+ let ndjsonWriteError;
1667
1686
  const fullLog = fs5.createWriteStream(ndjsonPath, { flags: "w" });
1687
+ fullLog.on("error", (err) => {
1688
+ ndjsonWriteFailed = true;
1689
+ ndjsonWriteError = err instanceof Error ? err.message : String(err);
1690
+ });
1668
1691
  const resultTexts = [];
1669
1692
  outcome = "failed";
1670
1693
  outcomeKind = "generic_failed";
1671
1694
  errorMessage = void 0;
1672
1695
  tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
1673
1696
  messageCount = 0;
1674
- let ndjsonWriteFailed = false;
1675
- let ndjsonWriteError;
1676
1697
  let sawMutatingTool = false;
1677
1698
  let sawTerminalSuccess = false;
1678
1699
  let noWorkSuccess = false;
@@ -1747,7 +1768,13 @@ async function runAgent(opts) {
1747
1768
  if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
1748
1769
  queryOptions.maxTurns = opts.maxTurns;
1749
1770
  }
1750
- if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
1771
+ if (opts.reasoningEffort !== void 0 && opts.reasoningEffort !== null) {
1772
+ if (opts.reasoningEffort === "off") {
1773
+ } else {
1774
+ const budget = REASONING_BUDGETS[opts.reasoningEffort];
1775
+ if (budget) queryOptions.maxThinkingTokens = budget;
1776
+ }
1777
+ } else if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
1751
1778
  queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
1752
1779
  }
1753
1780
  if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
@@ -1808,12 +1835,14 @@ async function runAgent(opts) {
1808
1835
  if (next.done) break;
1809
1836
  const msg = next.value;
1810
1837
  messageCount++;
1811
- try {
1812
- fullLog.write(`${JSON.stringify(msg)}
1838
+ if (!ndjsonWriteFailed) {
1839
+ try {
1840
+ fullLog.write(`${JSON.stringify(msg)}
1813
1841
  `);
1814
- } catch (e) {
1815
- ndjsonWriteFailed = true;
1816
- ndjsonWriteError = e instanceof Error ? e.message : String(e);
1842
+ } catch (e) {
1843
+ ndjsonWriteFailed = true;
1844
+ ndjsonWriteError = e instanceof Error ? e.message : String(e);
1845
+ }
1817
1846
  }
1818
1847
  const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
1819
1848
  if (line) process.stdout.write(`${line}
@@ -2945,7 +2974,7 @@ function loadProfile(profilePath) {
2945
2974
  const preNames = new Set(profile.scripts.preflight.map((e) => e.script).filter(Boolean));
2946
2975
  const postNames = profile.scripts.postflight.map((e) => e.script).filter(Boolean);
2947
2976
  const needsState = postNames.includes("writeJobStateFile") || postNames.includes("parseJobStateFromAgentResult");
2948
- const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript"];
2977
+ const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript", "runScheduledExecutableTick"];
2949
2978
  if (needsState && !STATE_LOADERS.some((s) => preNames.has(s))) {
2950
2979
  throw new ProfileError(
2951
2980
  profilePath,
@@ -5574,10 +5603,10 @@ var init_state2 = __esm({
5574
5603
  "use strict";
5575
5604
  VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5576
5605
  GoalStateError = class extends Error {
5577
- constructor(path44, message) {
5578
- super(`Invalid goal state at ${path44}:
5606
+ constructor(path45, message) {
5607
+ super(`Invalid goal state at ${path45}:
5579
5608
  ${message}`);
5580
- this.path = path44;
5609
+ this.path = path45;
5581
5610
  this.name = "GoalStateError";
5582
5611
  }
5583
5612
  path;
@@ -7891,9 +7920,9 @@ var init_dispatchDutyFileTicks = __esm({
7891
7920
  init_dutyFolders();
7892
7921
  init_issue();
7893
7922
  init_job();
7894
- init_scheduleEvery();
7895
7923
  init_jobState();
7896
7924
  init_planTaskJobs();
7925
+ init_scheduleEvery();
7897
7926
  dispatchDutyFileTicks = async (ctx, _profile, args) => {
7898
7927
  ctx.skipAgent = true;
7899
7928
  const targetExecutable = String(args?.targetExecutable ?? "");
@@ -7986,7 +8015,7 @@ var init_dispatchDutyFileTicks = __esm({
7986
8015
  continue;
7987
8016
  }
7988
8017
  const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
7989
- const cliArgs = config.executable ? {} : { [slugArg]: slug };
8018
+ const cliArgs = { [slugArg]: slug };
7990
8019
  process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
7991
8020
  `);
7992
8021
  try {
@@ -12352,11 +12381,46 @@ var init_runPreviewBuild = __esm({
12352
12381
  }
12353
12382
  });
12354
12383
 
12355
- // src/scripts/runTickScript.ts
12384
+ // src/scripts/tickShellRunner.ts
12356
12385
  import { spawnSync as spawnSync2 } from "child_process";
12357
- import * as fs36 from "fs";
12358
- import * as path35 from "path";
12359
- function buildChildEnv(parent, force) {
12386
+ function runTickShellAndParse(opts) {
12387
+ const childEnv = buildTickChildEnv(process.env, opts.force);
12388
+ const result = spawnSync2("bash", [opts.scriptPath], {
12389
+ cwd: opts.ctx.cwd,
12390
+ env: childEnv,
12391
+ stdio: ["ignore", "pipe", "pipe"],
12392
+ encoding: "utf-8",
12393
+ timeout: 5 * 60 * 1e3,
12394
+ maxBuffer: 16 * 1024 * 1024
12395
+ });
12396
+ if (result.stdout) process.stdout.write(result.stdout);
12397
+ if (result.stderr) process.stderr.write(result.stderr);
12398
+ if (result.error) {
12399
+ opts.ctx.output.exitCode = 99;
12400
+ opts.ctx.output.reason = `${opts.displayName}: spawn error: ${result.error.message}`;
12401
+ return;
12402
+ }
12403
+ if (result.signal) {
12404
+ opts.ctx.output.exitCode = 124;
12405
+ opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}killed by ${result.signal} (likely 5min timeout)`;
12406
+ return;
12407
+ }
12408
+ if (result.status !== 0) {
12409
+ opts.ctx.output.exitCode = result.status ?? 99;
12410
+ opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}exited ${result.status}`;
12411
+ return;
12412
+ }
12413
+ const prevRev = opts.loaded.state.rev ?? 0;
12414
+ const parsed = extractNextStateFromText(result.stdout ?? "", opts.fenceLabel, prevRev);
12415
+ if (parsed.error) {
12416
+ opts.ctx.data.nextStateParseError = parsed.error;
12417
+ opts.ctx.output.exitCode = 1;
12418
+ opts.ctx.output.reason = `${opts.displayName}: ${parsed.error}`;
12419
+ return;
12420
+ }
12421
+ opts.ctx.data.nextJobState = parsed.envelope;
12422
+ }
12423
+ function buildTickChildEnv(parent, force) {
12360
12424
  const allow = /* @__PURE__ */ new Set([
12361
12425
  "PATH",
12362
12426
  "HOME",
@@ -12367,45 +12431,104 @@ function buildChildEnv(parent, force) {
12367
12431
  "LANG",
12368
12432
  "LC_ALL",
12369
12433
  "TERM",
12370
- // GitHub auth — `gh` reads these.
12371
12434
  "GH_TOKEN",
12372
12435
  "GH_PAT",
12373
12436
  "GITHUB_TOKEN",
12374
- // CI metadata commonly read by tick scripts (`gh repo view`,
12375
- // workflow run links, etc.). All public values from GitHub Actions.
12376
- "GITHUB_ACTIONS",
12377
- "GITHUB_ACTOR",
12378
12437
  "GITHUB_REPOSITORY",
12379
- "GITHUB_REPOSITORY_OWNER",
12380
12438
  "GITHUB_REF",
12381
12439
  "GITHUB_SHA",
12382
12440
  "GITHUB_RUN_ID",
12383
- "GITHUB_RUN_NUMBER",
12441
+ "GITHUB_RUN_ATTEMPT",
12384
12442
  "GITHUB_WORKFLOW",
12385
- "GITHUB_JOB",
12386
- "GITHUB_SERVER_URL",
12387
- "GITHUB_API_URL",
12388
- "GITHUB_EVENT_NAME",
12389
- "RUNNER_OS",
12390
- "RUNNER_ARCH"
12443
+ "GITHUB_ACTIONS",
12444
+ "CI",
12445
+ "KODY_DRY_RUN",
12446
+ "KODY_NO_COMMIT"
12391
12447
  ]);
12392
- const out = {};
12448
+ const env = {};
12393
12449
  for (const [key, value] of Object.entries(parent)) {
12394
12450
  if (value === void 0) continue;
12395
- if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) {
12396
- out[key] = value;
12397
- }
12451
+ if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) env[key] = value;
12398
12452
  }
12399
- if (force) out.KODY_FORCE = "1";
12400
- return out;
12453
+ if (force) env.KODY_FORCE = "1";
12454
+ return env;
12401
12455
  }
12456
+ var init_tickShellRunner = __esm({
12457
+ "src/scripts/tickShellRunner.ts"() {
12458
+ "use strict";
12459
+ init_parseJobStateFromAgentResult();
12460
+ }
12461
+ });
12462
+
12463
+ // src/scripts/runScheduledExecutableTick.ts
12464
+ import * as fs36 from "fs";
12465
+ import * as path35 from "path";
12466
+ var runScheduledExecutableTick;
12467
+ var init_runScheduledExecutableTick = __esm({
12468
+ "src/scripts/runScheduledExecutableTick.ts"() {
12469
+ "use strict";
12470
+ init_dutyFolders();
12471
+ init_jobState();
12472
+ init_tickShellRunner();
12473
+ runScheduledExecutableTick = async (ctx, profile, args) => {
12474
+ ctx.skipAgent = true;
12475
+ const jobsDir = String(args?.jobsDir ?? ".kody/duties");
12476
+ const slugArg = String(args?.slugArg ?? "duty");
12477
+ const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
12478
+ const shell = String(args?.shell ?? "tick.sh");
12479
+ const slug = String(ctx.args[slugArg] ?? "").trim();
12480
+ if (!slug) {
12481
+ ctx.output.exitCode = 99;
12482
+ ctx.output.reason = `runScheduledExecutableTick: ctx.args.${slugArg} must be non-empty duty slug`;
12483
+ return;
12484
+ }
12485
+ const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12486
+ if (!duty) {
12487
+ ctx.output.exitCode = 99;
12488
+ ctx.output.reason = `runScheduledExecutableTick: duty folder not found or incomplete: ${path35.join(jobsDir, slug)}`;
12489
+ return;
12490
+ }
12491
+ const shellPath = path35.join(profile.dir, shell);
12492
+ if (!fs36.existsSync(shellPath)) {
12493
+ ctx.output.exitCode = 99;
12494
+ ctx.output.reason = `runScheduledExecutableTick: shell not found: ${shell} (looked in ${profile.dir})`;
12495
+ return;
12496
+ }
12497
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
12498
+ let loaded;
12499
+ try {
12500
+ loaded = await backend.load(slug);
12501
+ } catch (err) {
12502
+ ctx.output.exitCode = 99;
12503
+ ctx.output.reason = `runScheduledExecutableTick: state load failed: ${err instanceof Error ? err.message : String(err)}`;
12504
+ return;
12505
+ }
12506
+ ctx.data.jobSlug = slug;
12507
+ ctx.data.dutySlug = slug;
12508
+ ctx.data.executableSlug = profile.name;
12509
+ ctx.data.jobState = loaded;
12510
+ runTickShellAndParse({
12511
+ ctx,
12512
+ loaded,
12513
+ scriptPath: shellPath,
12514
+ displayName: `runScheduledExecutableTick: ${shell}`,
12515
+ fenceLabel,
12516
+ force: Boolean(ctx.args.force)
12517
+ });
12518
+ };
12519
+ }
12520
+ });
12521
+
12522
+ // src/scripts/runTickScript.ts
12523
+ import * as fs37 from "fs";
12524
+ import * as path36 from "path";
12402
12525
  var runTickScript;
12403
12526
  var init_runTickScript = __esm({
12404
12527
  "src/scripts/runTickScript.ts"() {
12405
12528
  "use strict";
12406
12529
  init_dutyFolders();
12407
12530
  init_jobState();
12408
- init_parseJobStateFromAgentResult();
12531
+ init_tickShellRunner();
12409
12532
  runTickScript = async (ctx, _profile, args) => {
12410
12533
  ctx.skipAgent = true;
12411
12534
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -12417,10 +12540,10 @@ var init_runTickScript = __esm({
12417
12540
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
12418
12541
  return;
12419
12542
  }
12420
- const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
12543
+ const duty = readDutyFolder(path36.join(ctx.cwd, jobsDir), slug);
12421
12544
  if (!duty) {
12422
12545
  ctx.output.exitCode = 99;
12423
- ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path35.join(ctx.cwd, jobsDir, slug)}`;
12546
+ ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path36.join(ctx.cwd, jobsDir, slug)}`;
12424
12547
  return;
12425
12548
  }
12426
12549
  const tickScript = duty.config.tickScript;
@@ -12429,8 +12552,8 @@ var init_runTickScript = __esm({
12429
12552
  ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript\` in profile.json \u2014 route via duty-tick instead`;
12430
12553
  return;
12431
12554
  }
12432
- const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
12433
- if (!fs36.existsSync(scriptPath)) {
12555
+ const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
12556
+ if (!fs37.existsSync(scriptPath)) {
12434
12557
  ctx.output.exitCode = 99;
12435
12558
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
12436
12559
  return;
@@ -12446,46 +12569,15 @@ var init_runTickScript = __esm({
12446
12569
  }
12447
12570
  ctx.data.jobSlug = slug;
12448
12571
  ctx.data.jobState = loaded;
12449
- const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
12450
- const result = spawnSync2("bash", [scriptPath], {
12451
- cwd: ctx.cwd,
12452
- env: childEnv,
12453
- stdio: ["ignore", "pipe", "pipe"],
12454
- encoding: "utf-8",
12455
- timeout: 5 * 60 * 1e3,
12456
- // Default maxBuffer is 1MB — a chatty `gh pr list --json …` over a
12457
- // busy repo (or an accidental `set -x`) can blow that and silently
12458
- // truncate stdout, which is the exact "silent state drop" failure
12459
- // mode this whole executable was written to prevent. 16MB is well
12460
- // above any realistic tick output.
12461
- maxBuffer: 16 * 1024 * 1024
12572
+ runTickShellAndParse({
12573
+ ctx,
12574
+ loaded,
12575
+ scriptPath,
12576
+ displayName: "runTickScript",
12577
+ reasonSubject: tickScript,
12578
+ fenceLabel,
12579
+ force: Boolean(ctx.args.force)
12462
12580
  });
12463
- if (result.stdout) process.stdout.write(result.stdout);
12464
- if (result.stderr) process.stderr.write(result.stderr);
12465
- if (result.error) {
12466
- ctx.output.exitCode = 99;
12467
- ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
12468
- return;
12469
- }
12470
- if (result.signal) {
12471
- ctx.output.exitCode = 124;
12472
- ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
12473
- return;
12474
- }
12475
- if (result.status !== 0) {
12476
- ctx.output.exitCode = result.status ?? 99;
12477
- ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
12478
- return;
12479
- }
12480
- const prevRev = loaded.state.rev ?? 0;
12481
- const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
12482
- if (parsed.error) {
12483
- ctx.data.nextStateParseError = parsed.error;
12484
- ctx.output.exitCode = 1;
12485
- ctx.output.reason = `runTickScript: ${parsed.error}`;
12486
- return;
12487
- }
12488
- ctx.data.nextJobState = parsed.envelope;
12489
12581
  };
12490
12582
  }
12491
12583
  });
@@ -13394,7 +13486,7 @@ var init_writeJobStateFile = __esm({
13394
13486
  });
13395
13487
 
13396
13488
  // src/scripts/writeRunSummary.ts
13397
- import * as fs37 from "fs";
13489
+ import * as fs38 from "fs";
13398
13490
  var writeRunSummary;
13399
13491
  var init_writeRunSummary = __esm({
13400
13492
  "src/scripts/writeRunSummary.ts"() {
@@ -13420,7 +13512,7 @@ var init_writeRunSummary = __esm({
13420
13512
  if (reason) lines.push(`- **Reason:** ${reason}`);
13421
13513
  lines.push("");
13422
13514
  try {
13423
- fs37.appendFileSync(summaryPath, `${lines.join("\n")}
13515
+ fs38.appendFileSync(summaryPath, `${lines.join("\n")}
13424
13516
  `);
13425
13517
  } catch {
13426
13518
  }
@@ -13507,6 +13599,7 @@ var init_scripts = __esm({
13507
13599
  init_reviewFlow();
13508
13600
  init_runFlow();
13509
13601
  init_runPreviewBuild();
13602
+ init_runScheduledExecutableTick();
13510
13603
  init_runTickScript();
13511
13604
  init_saveGoalState();
13512
13605
  init_saveTaskState();
@@ -13565,6 +13658,7 @@ var init_scripts = __esm({
13565
13658
  dispatchDutyFileTicks,
13566
13659
  planTaskJobs,
13567
13660
  dispatchNextTaskJob,
13661
+ runScheduledExecutableTick,
13568
13662
  runTickScript,
13569
13663
  runPreviewBuild,
13570
13664
  loadGoalState,
@@ -13627,8 +13721,8 @@ var init_scripts = __esm({
13627
13721
  });
13628
13722
 
13629
13723
  // src/staff.ts
13630
- import * as fs38 from "fs";
13631
- import * as path36 from "path";
13724
+ import * as fs39 from "fs";
13725
+ import * as path37 from "path";
13632
13726
  function stripFrontmatter(raw) {
13633
13727
  const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
13634
13728
  return (match ? match[1] : raw).trim();
@@ -13636,9 +13730,9 @@ function stripFrontmatter(raw) {
13636
13730
  function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
13637
13731
  const trimmed = slug.trim();
13638
13732
  if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
13639
- const staffPath = path36.join(cwd, staffDir, `${trimmed}.md`);
13640
- if (fs38.existsSync(staffPath)) {
13641
- const body = stripFrontmatter(fs38.readFileSync(staffPath, "utf-8"));
13733
+ const staffPath = path37.join(cwd, staffDir, `${trimmed}.md`);
13734
+ if (fs39.existsSync(staffPath)) {
13735
+ const body = stripFrontmatter(fs39.readFileSync(staffPath, "utf-8"));
13642
13736
  if (body) return body;
13643
13737
  const builtinForEmpty = BUILTIN_PERSONAS[trimmed];
13644
13738
  if (builtinForEmpty) return builtinForEmpty;
@@ -13751,8 +13845,8 @@ var init_tools = __esm({
13751
13845
 
13752
13846
  // src/executor.ts
13753
13847
  import { spawn as spawn7 } from "child_process";
13754
- import * as fs39 from "fs";
13755
- import * as path37 from "path";
13848
+ import * as fs40 from "fs";
13849
+ import * as path38 from "path";
13756
13850
  function isMutatingPostflight(scriptName) {
13757
13851
  return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
13758
13852
  }
@@ -13865,17 +13959,7 @@ async function runExecutable(profileName, input) {
13865
13959
  reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}`
13866
13960
  });
13867
13961
  }
13868
- let litellm = null;
13869
- if (profileName !== "preview-build") {
13870
- try {
13871
- litellm = await startLitellmIfNeeded(model, input.cwd);
13872
- } catch (err) {
13873
- return finishAndEnd({
13874
- exitCode: 99,
13875
- reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}`
13876
- });
13877
- }
13878
- }
13962
+ let litellm;
13879
13963
  const ctx = {
13880
13964
  args,
13881
13965
  cwd: input.cwd,
@@ -13903,16 +13987,23 @@ async function runExecutable(profileName, input) {
13903
13987
  })
13904
13988
  };
13905
13989
  })() : null;
13906
- const ndjsonDir = path37.join(input.cwd, ".kody");
13990
+ const ndjsonDir = path38.join(input.cwd, ".kody");
13907
13991
  const personaSlug = typeof profile.staff === "string" && profile.staff.length > 0 ? profile.staff : typeof ctx.data.jobPersona === "string" && ctx.data.jobPersona.length > 0 ? ctx.data.jobPersona : null;
13908
13992
  const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
13909
13993
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
13910
13994
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
13911
13995
  const invokeAgent = async (prompt) => {
13912
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
13996
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path38.isAbsolute(p) ? p : path38.resolve(profile.dir, p)).filter((p) => p.length > 0);
13913
13997
  const syntheticPath = ctx.data.syntheticPluginPath;
13914
13998
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
13915
13999
  const agents = loadSubagents(profile);
14000
+ if (litellm === void 0) {
14001
+ try {
14002
+ litellm = await startLitellmIfNeeded(model, input.cwd);
14003
+ } catch (err) {
14004
+ throw new Error(`litellm startup failed: ${err instanceof Error ? err.message : String(err)}`);
14005
+ }
14006
+ }
13916
14007
  const lm = litellm;
13917
14008
  return runAgent({
13918
14009
  prompt,
@@ -14020,7 +14111,14 @@ async function runExecutable(profileName, input) {
14020
14111
  });
14021
14112
  }
14022
14113
  emitEvent(input.cwd, { executable: profileName, kind: "agent_start" });
14023
- agentResult = await invokeAgent(prompt);
14114
+ try {
14115
+ agentResult = await invokeAgent(prompt);
14116
+ } catch (err) {
14117
+ return finishAndEnd({
14118
+ exitCode: 99,
14119
+ reason: err instanceof Error ? err.message : String(err)
14120
+ });
14121
+ }
14024
14122
  emitEvent(input.cwd, {
14025
14123
  executable: profileName,
14026
14124
  kind: "agent_end",
@@ -14274,17 +14372,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
14274
14372
  function resolveProfilePath(profileName) {
14275
14373
  const found = resolveExecutable(profileName);
14276
14374
  if (found) return found;
14277
- const here = path37.dirname(new URL(import.meta.url).pathname);
14375
+ const here = path38.dirname(new URL(import.meta.url).pathname);
14278
14376
  const candidates = [
14279
- path37.join(here, "executables", profileName, "profile.json"),
14377
+ path38.join(here, "executables", profileName, "profile.json"),
14280
14378
  // same-dir sibling (dev)
14281
- path37.join(here, "..", "executables", profileName, "profile.json"),
14379
+ path38.join(here, "..", "executables", profileName, "profile.json"),
14282
14380
  // up one (prod: dist/bin → dist/executables)
14283
- path37.join(here, "..", "src", "executables", profileName, "profile.json")
14381
+ path38.join(here, "..", "src", "executables", profileName, "profile.json")
14284
14382
  // fallback
14285
14383
  ];
14286
14384
  for (const c of candidates) {
14287
- if (fs39.existsSync(c)) return c;
14385
+ if (fs40.existsSync(c)) return c;
14288
14386
  }
14289
14387
  return candidates[0];
14290
14388
  }
@@ -14382,8 +14480,8 @@ function resolveShellTimeoutMs(entry) {
14382
14480
  }
14383
14481
  async function runShellEntry(entry, ctx, profile) {
14384
14482
  const shellName = entry.shell;
14385
- const shellPath = path37.join(profile.dir, shellName);
14386
- if (!fs39.existsSync(shellPath)) {
14483
+ const shellPath = path38.join(profile.dir, shellName);
14484
+ if (!fs40.existsSync(shellPath)) {
14387
14485
  ctx.skipAgent = true;
14388
14486
  ctx.output.exitCode = 99;
14389
14487
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -14542,7 +14640,7 @@ __export(job_exports, {
14542
14640
  stableJobKey: () => stableJobKey,
14543
14641
  validateJob: () => validateJob
14544
14642
  });
14545
- import * as path38 from "path";
14643
+ import * as path39 from "path";
14546
14644
  function newJobId(flavor) {
14547
14645
  localJobSeq += 1;
14548
14646
  const runId = process.env.GITHUB_RUN_ID;
@@ -14579,7 +14677,7 @@ function validateJob(input) {
14579
14677
  async function runJob(job, base) {
14580
14678
  const valid = validateJob(job);
14581
14679
  const action = valid.action ?? valid.duty;
14582
- const projectDutiesRoot = path38.join(base.cwd, ".kody", "duties");
14680
+ const projectDutiesRoot = path39.join(base.cwd, ".kody", "duties");
14583
14681
  const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
14584
14682
  const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
14585
14683
  const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
@@ -14635,7 +14733,7 @@ async function runJob(job, base) {
14635
14733
  }
14636
14734
  function loadDutyContext(slug, cwd) {
14637
14735
  if (!slug) return null;
14638
- return readDutyFolder(path38.join(cwd, ".kody", "duties"), slug) ?? readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
14736
+ return readDutyFolder(path39.join(cwd, ".kody", "duties"), slug) ?? readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
14639
14737
  }
14640
14738
  function mintInstantJob(dispatch2, opts) {
14641
14739
  return {
@@ -14787,9 +14885,9 @@ function translateOpenAISseToBrain(opts) {
14787
14885
  }
14788
14886
 
14789
14887
  // src/servers/brain-serve.ts
14790
- import * as fs42 from "fs";
14888
+ import * as fs43 from "fs";
14791
14889
  import { createServer } from "http";
14792
- import * as path41 from "path";
14890
+ import * as path42 from "path";
14793
14891
 
14794
14892
  // src/chat/loop.ts
14795
14893
  init_agent();
@@ -15158,6 +15256,7 @@ async function runChatTurn(opts) {
15158
15256
  verbose: opts.verbose,
15159
15257
  quiet: opts.quiet,
15160
15258
  systemPromptAppend: systemPrompt,
15259
+ ...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {},
15161
15260
  // Let the agent clone + work on OTHER repos mid-conversation (a
15162
15261
  // repo-less Brain serves many). Enabled whenever we know where repos
15163
15262
  // live; grants read access to that root via additionalDirectories.
@@ -15330,8 +15429,8 @@ init_config();
15330
15429
 
15331
15430
  // src/kody-cli.ts
15332
15431
  import { execFileSync as execFileSync26 } from "child_process";
15333
- import * as fs40 from "fs";
15334
- import * as path39 from "path";
15432
+ import * as fs41 from "fs";
15433
+ import * as path40 from "path";
15335
15434
 
15336
15435
  // src/app-auth.ts
15337
15436
  import { createSign } from "crypto";
@@ -15926,9 +16025,9 @@ async function resolveAuthToken(env = process.env) {
15926
16025
  return void 0;
15927
16026
  }
15928
16027
  function detectPackageManager2(cwd) {
15929
- if (fs40.existsSync(path39.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
15930
- if (fs40.existsSync(path39.join(cwd, "yarn.lock"))) return "yarn";
15931
- if (fs40.existsSync(path39.join(cwd, "bun.lockb"))) return "bun";
16028
+ if (fs41.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
16029
+ if (fs41.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
16030
+ if (fs41.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
15932
16031
  return "npm";
15933
16032
  }
15934
16033
  function shellOut(cmd, args, cwd, stream = true) {
@@ -16015,11 +16114,11 @@ function configureGitIdentity(cwd) {
16015
16114
  }
16016
16115
  function postFailureTail(issueNumber, cwd, reason) {
16017
16116
  if (!issueNumber) return;
16018
- const logPath = path39.join(cwd, ".kody", "last-run.jsonl");
16117
+ const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
16019
16118
  let tail = "";
16020
16119
  try {
16021
- if (fs40.existsSync(logPath)) {
16022
- const content = fs40.readFileSync(logPath, "utf-8");
16120
+ if (fs41.existsSync(logPath)) {
16121
+ const content = fs41.readFileSync(logPath, "utf-8");
16023
16122
  tail = content.slice(-3e3);
16024
16123
  }
16025
16124
  } catch {
@@ -16044,7 +16143,7 @@ async function runCi(argv) {
16044
16143
  return 0;
16045
16144
  }
16046
16145
  const args = parseCiArgs(argv);
16047
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
16146
+ const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
16048
16147
  let earlyConfig;
16049
16148
  let earlyConfigError;
16050
16149
  try {
@@ -16057,9 +16156,9 @@ async function runCi(argv) {
16057
16156
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
16058
16157
  let manualWorkflowDispatch = false;
16059
16158
  let forceRunAction = null;
16060
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs40.existsSync(dispatchEventPath)) {
16159
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs41.existsSync(dispatchEventPath)) {
16061
16160
  try {
16062
- const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
16161
+ const evt = JSON.parse(fs41.readFileSync(dispatchEventPath, "utf-8"));
16063
16162
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
16064
16163
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
16065
16164
  const dutyInput = String(evt?.inputs?.duty ?? evt?.inputs?.executable ?? "").trim();
@@ -16359,16 +16458,16 @@ init_litellm();
16359
16458
  init_repoWorkspace();
16360
16459
 
16361
16460
  // src/scripts/brainTurnLog.ts
16362
- import * as fs41 from "fs";
16363
- import * as path40 from "path";
16461
+ import * as fs42 from "fs";
16462
+ import * as path41 from "path";
16364
16463
  var live = /* @__PURE__ */ new Map();
16365
16464
  function eventsPath(dir, chatId) {
16366
- return path40.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16465
+ return path41.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
16367
16466
  }
16368
16467
  function lastPersistedSeq(dir, chatId) {
16369
16468
  const p = eventsPath(dir, chatId);
16370
- if (!fs41.existsSync(p)) return 0;
16371
- const lines = fs41.readFileSync(p, "utf-8").split("\n").filter(Boolean);
16469
+ if (!fs42.existsSync(p)) return 0;
16470
+ const lines = fs42.readFileSync(p, "utf-8").split("\n").filter(Boolean);
16372
16471
  if (lines.length === 0) return 0;
16373
16472
  try {
16374
16473
  return JSON.parse(lines[lines.length - 1]).seq || 0;
@@ -16378,9 +16477,9 @@ function lastPersistedSeq(dir, chatId) {
16378
16477
  }
16379
16478
  function readSince(dir, chatId, since) {
16380
16479
  const p = eventsPath(dir, chatId);
16381
- if (!fs41.existsSync(p)) return [];
16480
+ if (!fs42.existsSync(p)) return [];
16382
16481
  const out = [];
16383
- for (const line of fs41.readFileSync(p, "utf-8").split("\n")) {
16482
+ for (const line of fs42.readFileSync(p, "utf-8").split("\n")) {
16384
16483
  if (!line) continue;
16385
16484
  try {
16386
16485
  const rec = JSON.parse(line);
@@ -16406,12 +16505,12 @@ function beginTurn(dir, chatId) {
16406
16505
  };
16407
16506
  live.set(chatId, state);
16408
16507
  const p = eventsPath(dir, chatId);
16409
- fs41.mkdirSync(path40.dirname(p), { recursive: true });
16508
+ fs42.mkdirSync(path41.dirname(p), { recursive: true });
16410
16509
  return (event) => {
16411
16510
  state.seq += 1;
16412
16511
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
16413
16512
  try {
16414
- fs41.appendFileSync(p, `${JSON.stringify(rec)}
16513
+ fs42.appendFileSync(p, `${JSON.stringify(rec)}
16415
16514
  `);
16416
16515
  } catch (err) {
16417
16516
  process.stderr.write(
@@ -16450,7 +16549,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
16450
16549
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
16451
16550
  };
16452
16551
  try {
16453
- fs41.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
16552
+ fs42.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
16454
16553
  `);
16455
16554
  } catch {
16456
16555
  }
@@ -16681,7 +16780,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16681
16780
  const repo = strField(body, "repo");
16682
16781
  const repoToken = strField(body, "repoToken");
16683
16782
  const sessionFile = sessionFilePath(opts.cwd, chatId);
16684
- fs42.mkdirSync(path41.dirname(sessionFile), { recursive: true });
16783
+ fs43.mkdirSync(path42.dirname(sessionFile), { recursive: true });
16685
16784
  appendTurn(sessionFile, {
16686
16785
  role: "user",
16687
16786
  content: message,
@@ -16728,7 +16827,7 @@ async function handleChatTurn(req, res, chatId, opts) {
16728
16827
  function buildServer(opts) {
16729
16828
  const runTurn = opts.runTurn ?? runChatTurn;
16730
16829
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
16731
- const reposRoot = opts.reposRoot ?? path41.join(path41.dirname(path41.resolve(opts.cwd)), "repos");
16830
+ const reposRoot = opts.reposRoot ?? path42.join(path42.dirname(path42.resolve(opts.cwd)), "repos");
16732
16831
  return createServer(async (req, res) => {
16733
16832
  if (!req.method || !req.url) {
16734
16833
  sendJson(res, 400, { error: "bad request" });
@@ -17321,14 +17420,14 @@ async function loadConfigSafe() {
17321
17420
 
17322
17421
  // src/chat-cli.ts
17323
17422
  import { execFileSync as execFileSync29 } from "child_process";
17324
- import * as fs44 from "fs";
17325
- import * as path43 from "path";
17423
+ import * as fs45 from "fs";
17424
+ import * as path44 from "path";
17326
17425
 
17327
17426
  // src/chat/modes/interactive.ts
17328
17427
  init_issue();
17329
17428
  import { execFileSync as execFileSync28 } from "child_process";
17330
- import * as fs43 from "fs";
17331
- import * as path42 from "path";
17429
+ import * as fs44 from "fs";
17430
+ import * as path43 from "path";
17332
17431
 
17333
17432
  // src/chat/inbox.ts
17334
17433
  import { execFileSync as execFileSync27 } from "child_process";
@@ -17461,7 +17560,8 @@ async function runInteractiveMode(opts) {
17461
17560
  sink: opts.sink,
17462
17561
  verbose: opts.verbose,
17463
17562
  quiet: opts.quiet,
17464
- invokeAgent: opts.invokeAgent
17563
+ invokeAgent: opts.invokeAgent,
17564
+ ...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {}
17465
17565
  });
17466
17566
  } catch (err) {
17467
17567
  const msg = err instanceof Error ? err.message : String(err);
@@ -17487,9 +17587,9 @@ function findNextUserTurn(turns, fromIdx) {
17487
17587
  return -1;
17488
17588
  }
17489
17589
  function commitTurn(cwd, sessionId, _verbose) {
17490
- const sessionRel = path42.relative(cwd, sessionFilePath(cwd, sessionId));
17491
- const eventsRel = path42.relative(cwd, eventsFilePath(cwd, sessionId));
17492
- const rels = [sessionRel, eventsRel].filter((p) => fs43.existsSync(path42.join(cwd, p)));
17590
+ const sessionRel = path43.relative(cwd, sessionFilePath(cwd, sessionId));
17591
+ const eventsRel = path43.relative(cwd, eventsFilePath(cwd, sessionId));
17592
+ const rels = [sessionRel, eventsRel].filter((p) => fs44.existsSync(path43.join(cwd, p)));
17493
17593
  if (rels.length === 0) return;
17494
17594
  const repository = process.env.GITHUB_REPOSITORY;
17495
17595
  if (!repository) {
@@ -17501,8 +17601,8 @@ function commitTurn(cwd, sessionId, _verbose) {
17501
17601
  }
17502
17602
  const branch = defaultBranch(cwd) ?? "main";
17503
17603
  for (const rel of rels) {
17504
- const repoPath = rel.split(path42.sep).join("/");
17505
- const localText = fs43.readFileSync(path42.join(cwd, rel), "utf-8");
17604
+ const repoPath = rel.split(path43.sep).join("/");
17605
+ const localText = fs44.readFileSync(path43.join(cwd, rel), "utf-8");
17506
17606
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
17507
17607
  }
17508
17608
  }
@@ -17600,11 +17700,17 @@ var CHAT_HELP = `kody chat \u2014 dashboard-driven chat session
17600
17700
 
17601
17701
  Usage:
17602
17702
  kody chat [--session <id>] [--message <text>] [--model <provider/model>]
17703
+ [--reasoning-effort <off|low|medium|high>]
17603
17704
  [--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
17604
17705
 
17605
- All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
17706
+ All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, REASONING_EFFORT, DASHBOARD_URL.
17606
17707
  CLI flags take precedence over env. SESSION_ID is required.
17607
17708
 
17709
+ Thinking level maps to the Claude Agent SDK's maxThinkingTokens (Anthropic
17710
+ extended thinking). Default is unset (no thinking \u2014 cheapest). Set via the
17711
+ dashboard's chat-level thinking dropdown, the REASONING_EFFORT env var,
17712
+ or this flag.
17713
+
17608
17714
  Exit codes:
17609
17715
  0 reply emitted successfully
17610
17716
  64 bad inputs (missing session, empty history)
@@ -17617,6 +17723,7 @@ function parseChatArgs(argv, env = process.env) {
17617
17723
  if (arg === "--session") result.sessionId = argv[++i];
17618
17724
  else if (arg === "--message") result.initMessage = argv[++i];
17619
17725
  else if (arg === "--model") result.model = argv[++i];
17726
+ else if (arg === "--reasoning-effort") result.reasoningEffort = parseReasoningEffort(argv[++i]) ?? void 0;
17620
17727
  else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
17621
17728
  else if (arg === "--cwd") result.cwd = argv[++i];
17622
17729
  else if (arg === "--verbose") result.verbose = true;
@@ -17629,22 +17736,23 @@ function parseChatArgs(argv, env = process.env) {
17629
17736
  result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
17630
17737
  result.model = result.model ?? env.MODEL ?? void 0;
17631
17738
  result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
17739
+ result.reasoningEffort = result.reasoningEffort ?? parseReasoningEffort(env.REASONING_EFFORT) ?? void 0;
17632
17740
  for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
17633
17741
  const v = result[key];
17634
17742
  if (typeof v === "string" && v.trim() === "") result[key] = void 0;
17635
17743
  }
17636
17744
  if (!result.sessionId && !result.errors.includes("__HELP__")) {
17637
- result.errors.push("--session <id> (or SESSION_ID env) is required");
17745
+ result.errors.push("--session <id> (or SESSION_ID env) is required)");
17638
17746
  }
17639
17747
  return result;
17640
17748
  }
17641
17749
  function commitChatFiles(cwd, sessionId, verbose) {
17642
- const sessionFile = path43.relative(cwd, sessionFilePath(cwd, sessionId));
17643
- const eventsFile = path43.relative(cwd, eventsFilePath(cwd, sessionId));
17750
+ const sessionFile = path44.relative(cwd, sessionFilePath(cwd, sessionId));
17751
+ const eventsFile = path44.relative(cwd, eventsFilePath(cwd, sessionId));
17644
17752
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
17645
- const tasksDir = path43.join(".kody", "tasks", safeSession);
17753
+ const tasksDir = path44.join(".kody", "tasks", safeSession);
17646
17754
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
17647
- const paths = candidatePaths.filter((p) => fs44.existsSync(path43.join(cwd, p)));
17755
+ const paths = candidatePaths.filter((p) => fs45.existsSync(path44.join(cwd, p)));
17648
17756
  if (paths.length === 0) return;
17649
17757
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
17650
17758
  try {
@@ -17688,7 +17796,7 @@ async function runChat(argv) {
17688
17796
  ${CHAT_HELP}`);
17689
17797
  return 64;
17690
17798
  }
17691
- const cwd = args.cwd ? path43.resolve(args.cwd) : process.cwd();
17799
+ const cwd = args.cwd ? path44.resolve(args.cwd) : process.cwd();
17692
17800
  const sessionId = args.sessionId;
17693
17801
  const unpackedSecrets = unpackAllSecrets();
17694
17802
  if (unpackedSecrets > 0) {
@@ -17699,6 +17807,7 @@ ${CHAT_HELP}`);
17699
17807
  configureGitIdentity(cwd);
17700
17808
  const config = tryLoadConfig(cwd);
17701
17809
  const modelSpec = args.model ?? config?.agent.model ?? DEFAULT_MODEL2;
17810
+ const reasoningEffort = args.reasoningEffort ?? config?.agent.reasoningEffort ?? void 0;
17702
17811
  let model;
17703
17812
  try {
17704
17813
  model = parseProviderModel(modelSpec);
@@ -17740,7 +17849,7 @@ ${CHAT_HELP}`);
17740
17849
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
17741
17850
  const meta = readMeta(sessionFile);
17742
17851
  process.stdout.write(
17743
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs44.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
17852
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
17744
17853
  `
17745
17854
  );
17746
17855
  try {
@@ -17753,7 +17862,8 @@ ${CHAT_HELP}`);
17753
17862
  sink,
17754
17863
  meta,
17755
17864
  verbose: args.verbose,
17756
- quiet: args.quiet
17865
+ quiet: args.quiet,
17866
+ ...reasoningEffort ? { reasoningEffort } : {}
17757
17867
  });
17758
17868
  return result2.exitCode;
17759
17869
  }
@@ -17765,7 +17875,8 @@ ${CHAT_HELP}`);
17765
17875
  litellmUrl: litellm?.url ?? null,
17766
17876
  sink,
17767
17877
  verbose: args.verbose,
17768
- quiet: args.quiet
17878
+ quiet: args.quiet,
17879
+ ...reasoningEffort ? { reasoningEffort } : {}
17769
17880
  });
17770
17881
  commitChatFiles(cwd, sessionId, args.verbose ?? false);
17771
17882
  return result.exitCode;
@@ -17896,8 +18007,8 @@ var FlyClient = class {
17896
18007
  get fetch() {
17897
18008
  return this.opts.fetchImpl ?? fetch;
17898
18009
  }
17899
- async call(path44, init = {}) {
17900
- const res = await this.fetch(`${FLY_API_BASE}${path44}`, {
18010
+ async call(path45, init = {}) {
18011
+ const res = await this.fetch(`${FLY_API_BASE}${path45}`, {
17901
18012
  method: init.method ?? "GET",
17902
18013
  headers: {
17903
18014
  Authorization: `Bearer ${this.opts.token}`,
@@ -17908,7 +18019,7 @@ var FlyClient = class {
17908
18019
  if (res.status === 404 && init.allow404) return null;
17909
18020
  if (!res.ok) {
17910
18021
  const text = await res.text().catch(() => "");
17911
- throw new Error(`Fly API ${res.status} on ${path44}: ${text.slice(0, 200) || res.statusText}`);
18022
+ throw new Error(`Fly API ${res.status} on ${path45}: ${text.slice(0, 200) || res.statusText}`);
17912
18023
  }
17913
18024
  if (res.status === 204) return null;
17914
18025
  const raw = await res.text();
@@ -18134,7 +18245,9 @@ var PoolManager = class {
18134
18245
  const destroyedSurplus = await this.destroySurplus(surplus);
18135
18246
  const destroyed = destroyedTracked + destroyedSurplus;
18136
18247
  if (pruned > 0 || adopted > 0 || destroyed > 0) {
18137
- this.log(`resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`);
18248
+ this.log(
18249
+ `resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`
18250
+ );
18138
18251
  }
18139
18252
  await this.refill();
18140
18253
  }
@@ -18631,7 +18744,7 @@ async function poolServe() {
18631
18744
 
18632
18745
  // src/servers/runner-serve.ts
18633
18746
  import { spawn as spawn8 } from "child_process";
18634
- import * as fs45 from "fs";
18747
+ import * as fs46 from "fs";
18635
18748
  import { createServer as createServer5 } from "http";
18636
18749
  var DEFAULT_PORT2 = 8080;
18637
18750
  var DEFAULT_WORKDIR = "/workspace/repo";
@@ -18711,8 +18824,8 @@ async function defaultRunJob(job) {
18711
18824
  const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
18712
18825
  const branch = job.ref ?? "main";
18713
18826
  const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
18714
- fs45.rmSync(workdir, { recursive: true, force: true });
18715
- fs45.mkdirSync(workdir, { recursive: true });
18827
+ fs46.rmSync(workdir, { recursive: true, force: true });
18828
+ fs46.mkdirSync(workdir, { recursive: true });
18716
18829
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
18717
18830
  const interactive = job.mode === "interactive";
18718
18831
  const scheduled = job.mode === "scheduled";
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "preview-health",
3
+ "role": "watch",
4
+ "kind": "oneshot",
5
+ "describe": "Scan open PRs and dispatch mechanical preview repairs.",
6
+ "inputs": [],
7
+ "claudeCode": {
8
+ "model": "inherit",
9
+ "permissionMode": "default",
10
+ "maxTurns": 0,
11
+ "maxThinkingTokens": null,
12
+ "systemPromptAppend": null,
13
+ "tools": [],
14
+ "hooks": [],
15
+ "skills": [],
16
+ "commands": [],
17
+ "subagents": [],
18
+ "plugins": [],
19
+ "mcpServers": []
20
+ },
21
+ "cliTools": [
22
+ {
23
+ "name": "gh",
24
+ "install": {
25
+ "required": true,
26
+ "checkCommand": "command -v gh"
27
+ },
28
+ "verify": "gh auth status",
29
+ "usage": "Reads open PRs, reads the CTO trust ledger, dispatches Kody workflows, and posts PR audit/recommendation comments.",
30
+ "allowedUses": ["pr", "api", "issue", "workflow"]
31
+ }
32
+ ],
33
+ "inputArtifacts": [],
34
+ "outputArtifacts": [],
35
+ "scripts": {
36
+ "preflight": [
37
+ {
38
+ "shell": "tick.sh",
39
+ "with": {
40
+ "jobsDir": ".kody/duties",
41
+ "duty": "preview-health"
42
+ },
43
+ "timeoutSec": 300
44
+ }
45
+ ],
46
+ "postflight": []
47
+ }
48
+ }
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ jobs_dir="${1:-.kody/duties}"
5
+ duty="${2:-preview-health}"
6
+
7
+ export KODY_PREVIEW_HEALTH_JOBS_DIR="$jobs_dir"
8
+ export KODY_PREVIEW_HEALTH_DUTY="$duty"
9
+
10
+ python3 <<'PY'
11
+ import base64
12
+ import json
13
+ import os
14
+ import re
15
+ import subprocess
16
+ import sys
17
+ from datetime import datetime, timezone
18
+
19
+ DECISIONS_LABEL = "kody:cto-decisions"
20
+ LEDGER_START = "<!-- kody-cto-decisions:start -->"
21
+ LEDGER_END = "<!-- kody-cto-decisions:end -->"
22
+ VERBS = ("fix-ci", "sync", "resolve")
23
+ AUTO_VERBS = {"resolve"}
24
+ MAX_ACTIONS_PER_TICK = 5
25
+ FAIL_CONCLUSIONS = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"}
26
+ RUNNING_STATUSES = {"IN_PROGRESS", "QUEUED"}
27
+ STALE_THRESHOLD = 10
28
+ STATE_BRANCH = "kody-state"
29
+
30
+
31
+ def log(message):
32
+ print(f"[preview-health] {message}", file=sys.stderr)
33
+
34
+
35
+ def now_iso():
36
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
37
+
38
+
39
+ def run_gh(args, input_text=None, check=True):
40
+ result = subprocess.run(
41
+ ["gh", *args],
42
+ input=input_text,
43
+ text=True,
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.PIPE,
46
+ cwd=os.getcwd(),
47
+ )
48
+ if check and result.returncode != 0:
49
+ raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"gh {' '.join(args)} failed")
50
+ return result.stdout
51
+
52
+
53
+ def repo_slug():
54
+ owner = os.environ.get("KODY_CFG_GITHUB_OWNER", "").strip()
55
+ repo = os.environ.get("KODY_CFG_GITHUB_REPO", "").strip()
56
+ if owner and repo:
57
+ return f"{owner}/{repo}"
58
+ return run_gh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]).strip()
59
+
60
+
61
+ def default_branch(slug):
62
+ configured = os.environ.get("KODY_CFG_GIT_DEFAULTBRANCH", "").strip()
63
+ if configured:
64
+ return configured
65
+ return run_gh(["repo", "view", slug, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"]).strip()
66
+
67
+
68
+ def ensure_state_branch(slug):
69
+ if run_gh(["api", f"/repos/{slug}/git/ref/heads/{STATE_BRANCH}"], check=False).strip():
70
+ return
71
+ branch = default_branch(slug)
72
+ sha = run_gh(["api", f"/repos/{slug}/git/ref/heads/{branch}", "--jq", ".object.sha"]).strip()
73
+ payload = json.dumps({"ref": f"refs/heads/{STATE_BRANCH}", "sha": sha})
74
+ run_gh(["api", "--method", "POST", f"/repos/{slug}/git/refs", "--input", "-"], input_text=payload)
75
+
76
+
77
+ def initial_state():
78
+ return {"version": 1, "rev": 0, "cursor": "seed", "data": {}, "done": False}
79
+
80
+
81
+ def load_state(slug, path):
82
+ result = run_gh(
83
+ ["api", f"/repos/{slug}/contents/{path}?ref={STATE_BRANCH}"],
84
+ check=False,
85
+ )
86
+ if not result.strip():
87
+ return {"path": path, "sha": None, "state": initial_state(), "created": True}
88
+ obj = json.loads(result)
89
+ raw = base64.b64decode(obj["content"]).decode("utf-8")
90
+ return {"path": path, "sha": obj.get("sha"), "state": json.loads(raw), "created": False}
91
+
92
+
93
+ def state_unchanged(prev, next_state):
94
+ return (
95
+ prev.get("cursor") == next_state.get("cursor")
96
+ and prev.get("done") == next_state.get("done")
97
+ and prev.get("data") == next_state.get("data")
98
+ )
99
+
100
+
101
+ def save_state(slug, loaded, next_state):
102
+ if not loaded["created"] and state_unchanged(loaded["state"], next_state):
103
+ return False
104
+ ensure_state_branch(slug)
105
+ body = json.dumps(next_state, indent=2) + "\n"
106
+ payload = {
107
+ "message": f"chore(jobs): update state for {os.environ['KODY_PREVIEW_HEALTH_DUTY']} (rev {next_state['rev']})",
108
+ "content": base64.b64encode(body.encode("utf-8")).decode("ascii"),
109
+ "branch": STATE_BRANCH,
110
+ }
111
+ if loaded.get("sha"):
112
+ payload["sha"] = loaded["sha"]
113
+ result = run_gh(
114
+ ["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"],
115
+ input_text=json.dumps(payload),
116
+ check=False,
117
+ )
118
+ if result.strip():
119
+ return True
120
+ reloaded = load_state(slug, loaded["path"])
121
+ if not reloaded["created"] and state_unchanged(reloaded["state"], next_state):
122
+ return False
123
+ if reloaded.get("sha"):
124
+ payload["sha"] = reloaded["sha"]
125
+ else:
126
+ payload.pop("sha", None)
127
+ run_gh(["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"], input_text=json.dumps(payload))
128
+ return True
129
+
130
+
131
+ def read_ledger_modes():
132
+ modes = {verb: "ask" for verb in VERBS}
133
+ try:
134
+ raw = run_gh(["issue", "list", "--label", DECISIONS_LABEL, "--state", "open", "--limit", "20", "--json", "number,body"])
135
+ issues = json.loads(raw or "[]")
136
+ if not issues:
137
+ return modes
138
+ body = sorted(issues, key=lambda x: x.get("number", 10**9))[0].get("body", "")
139
+ if LEDGER_START not in body or LEDGER_END not in body:
140
+ return modes
141
+ inner = body.split(LEDGER_START, 1)[1].split(LEDGER_END, 1)[0]
142
+ match = re.search(r"```(?:json)?\s*(\{[\s\S]*?\})\s*```", inner)
143
+ if not match:
144
+ return modes
145
+ ledger = json.loads(match.group(1))
146
+ cto = ledger.get("staff", {}).get("cto", {})
147
+ for verb in VERBS:
148
+ if cto.get(verb, {}).get("mode") == "auto":
149
+ modes[verb] = "auto"
150
+ except Exception as err:
151
+ log(f"ledger read failed; treating all verbs as ask: {err}")
152
+ return modes
153
+
154
+
155
+ def ci_failing(rollup):
156
+ if not isinstance(rollup, list):
157
+ return False
158
+ has_fail = any(str(check.get("conclusion", "")) in FAIL_CONCLUSIONS for check in rollup if isinstance(check, dict))
159
+ running = any(str(check.get("status", "")) in RUNNING_STATUSES for check in rollup if isinstance(check, dict))
160
+ return has_fail and not running
161
+
162
+
163
+ def behind_by(slug, base, head):
164
+ try:
165
+ raw = run_gh(["api", f"repos/{slug}/compare/{base}...{head}", "--jq", ".behind_by"])
166
+ return int(raw.strip() or "0")
167
+ except Exception as err:
168
+ log(f"compare {base}...{head} failed: {err}")
169
+ return 0
170
+
171
+
172
+ def detect_repair(slug, pr):
173
+ mergeable = str(pr.get("mergeable") or "").upper()
174
+ if mergeable == "UNKNOWN":
175
+ return ("defer", f"PR #{pr['number']} mergeability still UNKNOWN; retry next tick.")
176
+ if mergeable == "CONFLICTING":
177
+ return ("resolve", f"PR #{pr['number']} has merge conflicts with `{pr['baseRefName']}`.")
178
+ if ci_failing(pr.get("statusCheckRollup")):
179
+ return ("fix-ci", f"PR #{pr['number']} has failing CI checks.")
180
+ drift = behind_by(slug, pr["baseRefName"], pr["headRefName"])
181
+ if drift >= STALE_THRESHOLD:
182
+ return ("sync", f"PR #{pr['number']}'s branch is {drift} commits behind `{pr['baseRefName']}`.")
183
+ return None
184
+
185
+
186
+ def duty_operator(jobs_dir, duty):
187
+ try:
188
+ with open(os.path.join(os.getcwd(), jobs_dir, duty, "profile.json"), "r", encoding="utf-8") as f:
189
+ profile = json.load(f)
190
+ mentions = profile.get("mentions", [])
191
+ return mentions[0] if isinstance(mentions, list) and mentions else ""
192
+ except Exception:
193
+ return ""
194
+
195
+
196
+ def post_comment(pr_number, body):
197
+ if os.environ.get("KODY_DRY_RUN") == "1":
198
+ log(f"[dry-run] would comment on #{pr_number}: {body.splitlines()[0]}")
199
+ return True
200
+ try:
201
+ run_gh(["pr", "comment", str(pr_number), "--body", body])
202
+ return True
203
+ except Exception as err:
204
+ log(f"comment failed on #{pr_number}: {err}")
205
+ return False
206
+
207
+
208
+ def recommend(pr_number, verb, reason, operator):
209
+ mention = f"@{operator} " if operator else ""
210
+ body = (
211
+ f"{mention}**CTO recommendation** - `{verb}`\n\n"
212
+ f"{reason} Recommended action: `{verb}` for PR #{pr_number}.\n\n"
213
+ "_Confirm in dashboard inbox or run the action manually. CTO will not act on its own._"
214
+ )
215
+ return post_comment(pr_number, body)
216
+
217
+
218
+ def auto_run(pr_number, verb, reason):
219
+ if os.environ.get("KODY_DRY_RUN") == "1":
220
+ log(f"[dry-run] would dispatch kody.yml executable={verb} issue_number={pr_number}")
221
+ else:
222
+ try:
223
+ run_gh(["workflow", "run", "kody.yml", "-f", f"executable={verb}", "-f", f"issue_number={pr_number}"])
224
+ except Exception as err:
225
+ log(f"workflow_dispatch failed #{pr_number} ({verb}): {err}")
226
+ return False
227
+ auto_reason = (
228
+ "Policy: preview-health auto-runs `resolve` for merge conflicts."
229
+ if verb in AUTO_VERBS
230
+ else f"Graduated: operator approved `{verb}` repeatedly. A **Reject** on any `{verb}` returns me asking."
231
+ )
232
+ return post_comment(pr_number, f"**CTO action** - `{verb}` dispatched\n\n{reason}\n\n{auto_reason}")
233
+
234
+
235
+ def print_row(pr, verb, fp, action, note):
236
+ print(f"| #{pr} | {verb} | {fp} | {action} | {note} |")
237
+
238
+
239
+ def main():
240
+ slug = repo_slug()
241
+ jobs_dir = os.environ["KODY_PREVIEW_HEALTH_JOBS_DIR"].rstrip("/")
242
+ duty = os.environ["KODY_PREVIEW_HEALTH_DUTY"]
243
+ state_path = f"{jobs_dir}/{duty}/state.json"
244
+ loaded = load_state(slug, state_path)
245
+ modes = read_ledger_modes()
246
+ operator = duty_operator(jobs_dir, duty)
247
+
248
+ prs = json.loads(
249
+ run_gh([
250
+ "pr",
251
+ "list",
252
+ "--state",
253
+ "open",
254
+ "--limit",
255
+ "100",
256
+ "--json",
257
+ "number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt",
258
+ ])
259
+ or "[]"
260
+ )
261
+
262
+ prior = loaded["state"].get("data", {}).get("prs", {})
263
+ open_numbers = {str(pr["number"]) for pr in prs}
264
+ next_prs = {key: value for key, value in prior.items() if key in open_numbers and isinstance(value, dict)}
265
+
266
+ print("| PR | verb | fingerprint | action | note |")
267
+ print("|----|------|-------------|--------|------|")
268
+
269
+ priority = {"resolve": 0, "fix-ci": 1, "sync": 2}
270
+ queue = []
271
+ for pr in prs:
272
+ if pr.get("isDraft"):
273
+ print_row(pr["number"], "-", "-", "skip", "draft")
274
+ continue
275
+ repair = detect_repair(slug, pr)
276
+ if not repair:
277
+ print_row(pr["number"], "-", "-", "skip", "healthy")
278
+ continue
279
+ verb, reason = repair
280
+ if verb == "defer":
281
+ print_row(pr["number"], "-", "-", "defer", "mergeable=UNKNOWN")
282
+ continue
283
+ queue.append((priority[verb], pr["number"], pr, verb, reason))
284
+
285
+ actions_taken = 0
286
+ for _, number, pr, verb, reason in sorted(queue):
287
+ key = str(number)
288
+ fp = f"{verb}|{pr.get('headRefOid', '')}"
289
+ graduated = verb in AUTO_VERBS or modes.get(verb) == "auto"
290
+ intended_stage = f"{verb}-auto" if graduated else f"{verb}-recommended"
291
+ prev = next_prs.get(key, {})
292
+ if prev.get("fp") == fp and prev.get("stage") == intended_stage:
293
+ print_row(number, verb, fp[:24], "skip", "dedup")
294
+ continue
295
+ if actions_taken >= MAX_ACTIONS_PER_TICK:
296
+ print_row(number, verb, fp[:24], "defer", "tick cap")
297
+ continue
298
+
299
+ ok = auto_run(number, verb, reason) if graduated else recommend(number, verb, reason, operator)
300
+ action = "auto-ran" if graduated and ok else "auto-failed" if graduated else "recommended" if ok else "recommend-failed"
301
+ if ok:
302
+ actions_taken += 1
303
+ next_prs[key] = {"fp": fp, "stage": intended_stage, "lastActAt": now_iso()}
304
+ print_row(number, verb, fp[:24], action, "auto" if graduated else "advisory")
305
+
306
+ next_state = {
307
+ "version": 1,
308
+ "rev": int(loaded["state"].get("rev", 0)) + 1,
309
+ "cursor": "idle",
310
+ "data": {"prs": next_prs, "lastFiredAt": now_iso()},
311
+ "done": False,
312
+ }
313
+ save_state(slug, loaded, next_state)
314
+ log(f"tick complete: {actions_taken} action(s), {len(next_prs)} tracked PR(s)")
315
+ print("KODY_SKIP_AGENT=true")
316
+
317
+
318
+ main()
319
+ PY
@@ -261,8 +261,10 @@ open_prepare_pr() {
261
261
  fi
262
262
 
263
263
  # Bump version files.
264
- local files
265
- mapfile -t files < <(resolve_version_files)
264
+ local files=()
265
+ while IFS= read -r f; do
266
+ files+=("$f")
267
+ done < <(resolve_version_files)
266
268
  local touched=()
267
269
  for f in "${files[@]}"; do
268
270
  local res
@@ -63,6 +63,11 @@
63
63
  "pattern": "^[^/]+/.+$",
64
64
  "description": "Base provider/model string, for example minimax/MiniMax-M3 or claude/claude-sonnet-4-6."
65
65
  },
66
+ "reasoningEffort": {
67
+ "type": "string",
68
+ "enum": ["off", "low", "medium", "high"],
69
+ "description": "Default thinking effort for the Claude Agent SDK. Maps to maxThinkingTokens. 'off' (default) skips the thinking block entirely — no reasoning preamble, no extra tokens. 'low'/'medium'/'high' enable extended thinking with budgets 2_048/10_000/32_000. Overridable per-session via the REASONING_EFFORT env var or the dashboard's chat-level thinking dropdown."
70
+ },
66
71
  "perExecutable": {
67
72
  "type": "object",
68
73
  "description": "Optional provider/model override by executable name.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.222",
3
+ "version": "0.4.223",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",