@kody-ade/kody-engine 0.3.50 → 0.3.52

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
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.3.50",
6
+ version: "0.3.52",
7
7
  description: "kody \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,7 +50,7 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/chat-cli.ts
53
- import { execFileSync as execFileSync25 } from "child_process";
53
+ import { execFileSync as execFileSync26 } from "child_process";
54
54
  import * as fs26 from "fs";
55
55
  import * as path23 from "path";
56
56
 
@@ -154,7 +154,7 @@ function loadConfig(projectDir = process.cwd()) {
154
154
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
155
155
  }
156
156
  const quality = raw.quality ?? {};
157
- const git4 = raw.git ?? {};
157
+ const git5 = raw.git ?? {};
158
158
  const github = raw.github ?? {};
159
159
  const agent = raw.agent ?? {};
160
160
  if (!agent.model || typeof agent.model !== "string") {
@@ -171,7 +171,7 @@ function loadConfig(projectDir = process.cwd()) {
171
171
  testUnit: typeof quality.testUnit === "string" ? quality.testUnit : ""
172
172
  },
173
173
  git: {
174
- defaultBranch: typeof git4.defaultBranch === "string" ? git4.defaultBranch : "main"
174
+ defaultBranch: typeof git5.defaultBranch === "string" ? git5.defaultBranch : "main"
175
175
  },
176
176
  github: {
177
177
  owner: String(github.owner),
@@ -605,7 +605,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
605
605
  }
606
606
 
607
607
  // src/kody-cli.ts
608
- import { execFileSync as execFileSync24 } from "child_process";
608
+ import { execFileSync as execFileSync25 } from "child_process";
609
609
  import * as fs25 from "fs";
610
610
  import * as path22 from "path";
611
611
 
@@ -1313,6 +1313,12 @@ function parseScriptList(p, key, raw) {
1313
1313
  if (r.with && typeof r.with === "object") {
1314
1314
  entry.with = r.with;
1315
1315
  }
1316
+ if (typeof r.timeoutSec === "number" && r.timeoutSec > 0) {
1317
+ if (!hasShell) {
1318
+ throw new ProfileError(p, `scripts.${key}[${i}] "timeoutSec" only applies to shell entries`);
1319
+ }
1320
+ entry.timeoutSec = r.timeoutSec;
1321
+ }
1316
1322
  out.push(entry);
1317
1323
  }
1318
1324
  return out;
@@ -4632,6 +4638,14 @@ function walkMd(root, visit) {
4632
4638
  }
4633
4639
  }
4634
4640
 
4641
+ // src/scripts/markFlowSuccess.ts
4642
+ var markFlowSuccess = async (ctx) => {
4643
+ const exit = ctx.output.exitCode;
4644
+ if (exit === void 0 || exit === 0) {
4645
+ ctx.data.agentDone = true;
4646
+ }
4647
+ };
4648
+
4635
4649
  // src/scripts/memorizeFlow.ts
4636
4650
  import { execFileSync as execFileSync15 } from "child_process";
4637
4651
  import * as fs22 from "fs";
@@ -5558,6 +5572,120 @@ function tryPostPr3(prNumber, body, cwd) {
5558
5572
  }
5559
5573
  }
5560
5574
 
5575
+ // src/scripts/revertFlow.ts
5576
+ import { execFileSync as execFileSync19 } from "child_process";
5577
+ var SHA_RE = /^[0-9a-f]{4,40}$/i;
5578
+ var revertFlow = async (ctx) => {
5579
+ const prNumber = ctx.args.pr;
5580
+ const pr = getPr(prNumber, ctx.cwd);
5581
+ if (pr.state !== "OPEN") {
5582
+ ctx.output.exitCode = 1;
5583
+ ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
5584
+ ctx.skipAgent = true;
5585
+ return;
5586
+ }
5587
+ ctx.data.pr = pr;
5588
+ ctx.data.commentTargetType = "pr";
5589
+ ctx.data.commentTargetNumber = prNumber;
5590
+ checkoutPrBranch(prNumber, ctx.cwd);
5591
+ ctx.data.branch = getCurrentBranch(ctx.cwd);
5592
+ const shasArg = String(ctx.args.shas ?? "").trim();
5593
+ if (!shasArg) {
5594
+ ctx.output.exitCode = 64;
5595
+ ctx.output.reason = "no commit SHAs provided \u2014 usage: @kody revert <sha> [<sha> \u2026]";
5596
+ ctx.skipAgent = true;
5597
+ tryPostPr4(prNumber, `\u26A0\uFE0F kody revert FAILED: ${ctx.output.reason}`, ctx.cwd);
5598
+ return;
5599
+ }
5600
+ const requested = shasArg.split(/\s+/).filter((s) => s.length > 0);
5601
+ const bad = requested.filter((s) => !SHA_RE.test(s));
5602
+ if (bad.length > 0) {
5603
+ ctx.output.exitCode = 64;
5604
+ ctx.output.reason = `not valid SHA-shaped tokens: ${bad.join(", ")}`;
5605
+ ctx.skipAgent = true;
5606
+ tryPostPr4(prNumber, `\u26A0\uFE0F kody revert FAILED: ${ctx.output.reason}`, ctx.cwd);
5607
+ return;
5608
+ }
5609
+ const resolved = [];
5610
+ const unreachable = [];
5611
+ for (const s of requested) {
5612
+ let full;
5613
+ try {
5614
+ full = git4(["rev-parse", "--verify", `${s}^{commit}`], ctx.cwd);
5615
+ } catch {
5616
+ unreachable.push(s);
5617
+ continue;
5618
+ }
5619
+ if (!isAncestorOfHead(full, ctx.cwd)) {
5620
+ unreachable.push(s);
5621
+ continue;
5622
+ }
5623
+ let subject = "";
5624
+ try {
5625
+ subject = git4(["log", "-1", "--format=%s", full], ctx.cwd);
5626
+ } catch {
5627
+ }
5628
+ resolved.push({ input: s, full, subject });
5629
+ }
5630
+ if (unreachable.length > 0) {
5631
+ ctx.output.exitCode = 64;
5632
+ ctx.output.reason = `commit(s) not found in this PR branch: ${unreachable.join(", ")}`;
5633
+ ctx.skipAgent = true;
5634
+ tryPostPr4(prNumber, `\u26A0\uFE0F kody revert FAILED: ${ctx.output.reason}`, ctx.cwd);
5635
+ return;
5636
+ }
5637
+ ctx.args.shas = resolved.map((r) => r.full).join(" ");
5638
+ ctx.data.commitMessage = buildCommitMessage(resolved);
5639
+ ctx.data.prSummary = buildPrSummary(resolved);
5640
+ ctx.skipAgent = true;
5641
+ const runUrl = getRunUrl();
5642
+ const runSuffix = runUrl ? `, run ${runUrl}` : "";
5643
+ const shaList = resolved.map((r) => `\`${r.full.slice(0, 7)}\``).join(", ");
5644
+ tryPostPr4(
5645
+ prNumber,
5646
+ `\u2699\uFE0F kody revert started on \`${ctx.data.branch}\`${runSuffix} \u2014 reverting ${shaList}`,
5647
+ ctx.cwd
5648
+ );
5649
+ };
5650
+ function buildCommitMessage(resolved) {
5651
+ if (resolved.length === 1) {
5652
+ const { full, subject } = resolved[0];
5653
+ return subject ? `revert: "${subject}" (${full.slice(0, 7)})` : `revert: ${full.slice(0, 7)}`;
5654
+ }
5655
+ const shas = resolved.map((r) => r.full.slice(0, 7)).join(", ");
5656
+ return `revert: ${resolved.length} commit(s) (${shas})`;
5657
+ }
5658
+ function buildPrSummary(resolved) {
5659
+ return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
5660
+ }
5661
+ function git4(args, cwd) {
5662
+ return execFileSync19("git", args, {
5663
+ encoding: "utf-8",
5664
+ timeout: 3e4,
5665
+ cwd,
5666
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
5667
+ stdio: ["pipe", "pipe", "pipe"]
5668
+ }).trim();
5669
+ }
5670
+ function isAncestorOfHead(sha, cwd) {
5671
+ try {
5672
+ execFileSync19("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
5673
+ cwd,
5674
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
5675
+ stdio: ["ignore", "ignore", "ignore"]
5676
+ });
5677
+ return true;
5678
+ } catch {
5679
+ return false;
5680
+ }
5681
+ }
5682
+ function tryPostPr4(prNumber, body, cwd) {
5683
+ try {
5684
+ postPrReviewComment(prNumber, body, cwd);
5685
+ } catch {
5686
+ }
5687
+ }
5688
+
5561
5689
  // src/scripts/resolvePreviewUrl.ts
5562
5690
  var DEFAULT_PREVIEW_URL = "http://localhost:3000";
5563
5691
  var resolvePreviewUrl = async (ctx) => {
@@ -5595,9 +5723,9 @@ var reviewFlow = async (ctx) => {
5595
5723
  ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
5596
5724
  const runUrl = getRunUrl();
5597
5725
  const runSuffix = runUrl ? `, run ${runUrl}` : "";
5598
- tryPostPr4(prNumber, `\u{1F440} kody review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
5726
+ tryPostPr5(prNumber, `\u{1F440} kody review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
5599
5727
  };
5600
- function tryPostPr4(prNumber, body, cwd) {
5728
+ function tryPostPr5(prNumber, body, cwd) {
5601
5729
  try {
5602
5730
  postPrReviewComment(prNumber, body, cwd);
5603
5731
  } catch {
@@ -5710,11 +5838,11 @@ var skipAgent = async (ctx) => {
5710
5838
  };
5711
5839
 
5712
5840
  // src/scripts/stageMergeConflicts.ts
5713
- import { execFileSync as execFileSync19 } from "child_process";
5841
+ import { execFileSync as execFileSync20 } from "child_process";
5714
5842
  var stageMergeConflicts = async (ctx) => {
5715
5843
  if (ctx.data.agentDone === false) return;
5716
5844
  try {
5717
- execFileSync19("git", ["add", "-A"], {
5845
+ execFileSync20("git", ["add", "-A"], {
5718
5846
  cwd: ctx.cwd,
5719
5847
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
5720
5848
  stdio: "pipe"
@@ -5724,7 +5852,7 @@ var stageMergeConflicts = async (ctx) => {
5724
5852
  };
5725
5853
 
5726
5854
  // src/scripts/startFlow.ts
5727
- import { execFileSync as execFileSync20 } from "child_process";
5855
+ import { execFileSync as execFileSync21 } from "child_process";
5728
5856
  var API_TIMEOUT_MS9 = 3e4;
5729
5857
  var startFlow = async (ctx, profile, _agentResult, args) => {
5730
5858
  const entry = args?.entry;
@@ -5758,7 +5886,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
5758
5886
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
5759
5887
  const body = `@kody ${next}`;
5760
5888
  try {
5761
- execFileSync20("gh", [sub, "comment", String(targetNumber), "--body", body], {
5889
+ execFileSync21("gh", [sub, "comment", String(targetNumber), "--body", body], {
5762
5890
  timeout: API_TIMEOUT_MS9,
5763
5891
  cwd,
5764
5892
  stdio: ["ignore", "pipe", "pipe"]
@@ -5772,7 +5900,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
5772
5900
  }
5773
5901
 
5774
5902
  // src/scripts/syncFlow.ts
5775
- import { execFileSync as execFileSync21 } from "child_process";
5903
+ import { execFileSync as execFileSync22 } from "child_process";
5776
5904
  var syncFlow = async (ctx, _profile, args) => {
5777
5905
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
5778
5906
  const prNumber = ctx.args.pr;
@@ -5810,7 +5938,7 @@ var syncFlow = async (ctx, _profile, args) => {
5810
5938
  if (announceOnSuccess) {
5811
5939
  ctx.output.exitCode = 0;
5812
5940
  ctx.output.reason = `already up to date with origin/${baseBranch}`;
5813
- tryPostPr5(prNumber, `\u2139\uFE0F kody sync: already up to date with origin/${baseBranch}`, ctx.cwd);
5941
+ tryPostPr6(prNumber, `\u2139\uFE0F kody sync: already up to date with origin/${baseBranch}`, ctx.cwd);
5814
5942
  }
5815
5943
  return;
5816
5944
  }
@@ -5827,7 +5955,7 @@ var syncFlow = async (ctx, _profile, args) => {
5827
5955
  ctx.output.reason = `merged origin/${baseBranch} into ${ctx.data.branch}`;
5828
5956
  const runUrl = getRunUrl();
5829
5957
  const runSuffix = runUrl ? ` ([logs](${runUrl}))` : "";
5830
- tryPostPr5(
5958
+ tryPostPr6(
5831
5959
  prNumber,
5832
5960
  `\u2705 kody sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`,
5833
5961
  ctx.cwd
@@ -5840,11 +5968,11 @@ function bail2(ctx, prNumber, reason) {
5840
5968
  ctx.output.reason = reason;
5841
5969
  const runUrl = getRunUrl();
5842
5970
  const runSuffix = runUrl ? ` ([logs](${runUrl}))` : "";
5843
- tryPostPr5(prNumber, `\u274C kody sync could not complete${runSuffix}: ${reason}`, ctx.cwd);
5971
+ tryPostPr6(prNumber, `\u274C kody sync could not complete${runSuffix}: ${reason}`, ctx.cwd);
5844
5972
  }
5845
5973
  function revParseHead(cwd) {
5846
5974
  try {
5847
- return execFileSync21("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5975
+ return execFileSync22("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5848
5976
  } catch {
5849
5977
  return "";
5850
5978
  }
@@ -5852,16 +5980,16 @@ function revParseHead(cwd) {
5852
5980
  function pushBranch(branch, cwd) {
5853
5981
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
5854
5982
  try {
5855
- execFileSync21("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
5983
+ execFileSync22("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
5856
5984
  } catch {
5857
- execFileSync21("git", ["push", "--force-with-lease", "-u", "origin", branch], {
5985
+ execFileSync22("git", ["push", "--force-with-lease", "-u", "origin", branch], {
5858
5986
  cwd,
5859
5987
  env,
5860
5988
  stdio: ["ignore", "pipe", "pipe"]
5861
5989
  });
5862
5990
  }
5863
5991
  }
5864
- function tryPostPr5(prNumber, body, cwd) {
5992
+ function tryPostPr6(prNumber, body, cwd) {
5865
5993
  try {
5866
5994
  postPrReviewComment(prNumber, body, cwd);
5867
5995
  } catch {
@@ -5965,7 +6093,7 @@ var verify = async (ctx) => {
5965
6093
  };
5966
6094
 
5967
6095
  // src/scripts/waitForCi.ts
5968
- import { execFileSync as execFileSync22 } from "child_process";
6096
+ import { execFileSync as execFileSync23 } from "child_process";
5969
6097
  var API_TIMEOUT_MS10 = 3e4;
5970
6098
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
5971
6099
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -5996,7 +6124,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5996
6124
  const summary = summarize(rows);
5997
6125
  if (summary !== lastSummary) {
5998
6126
  lastSummary = summary;
5999
- tryPostPr6(prNumber, `\u23F3 kody waitForCi: ${summary}`, ctx.cwd);
6127
+ tryPostPr7(prNumber, `\u23F3 kody waitForCi: ${summary}`, ctx.cwd);
6000
6128
  }
6001
6129
  const failed = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
6002
6130
  const pending = rows.filter((r) => r.bucket === "pending");
@@ -6008,7 +6136,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
6008
6136
  failedChecks: detail,
6009
6137
  prUrl
6010
6138
  });
6011
- tryPostPr6(
6139
+ tryPostPr7(
6012
6140
  prNumber,
6013
6141
  `\u{1F6D1} kody waitForCi: giving up after ${fixCiAttempts} fix-ci attempts. Failed: ${detail}`,
6014
6142
  ctx.cwd
@@ -6020,7 +6148,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
6020
6148
  maxAttempts: maxFixCiAttempts,
6021
6149
  prUrl
6022
6150
  });
6023
- tryPostPr6(
6151
+ tryPostPr7(
6024
6152
  prNumber,
6025
6153
  `\u274C kody waitForCi: CI failed (attempt ${fixCiAttempts + 1}/${maxFixCiAttempts}). Dispatching fix-ci. Failed: ${detail}`,
6026
6154
  ctx.cwd
@@ -6030,7 +6158,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
6030
6158
  }
6031
6159
  if (pending.length === 0) {
6032
6160
  ctx.data.action = mkAction("CI_PASSED", { checks: rows.length, prUrl });
6033
- tryPostPr6(prNumber, `\u2705 kody waitForCi: all ${rows.length} checks green on PR #${prNumber}`, ctx.cwd);
6161
+ tryPostPr7(prNumber, `\u2705 kody waitForCi: all ${rows.length} checks green on PR #${prNumber}`, ctx.cwd);
6034
6162
  return;
6035
6163
  }
6036
6164
  await sleep(pollSeconds * 1e3);
@@ -6039,11 +6167,11 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
6039
6167
  reason: `CI did not complete within ${timeoutMinutes} minutes`,
6040
6168
  prUrl
6041
6169
  });
6042
- tryPostPr6(prNumber, `\u231B kody waitForCi: timed out after ${timeoutMinutes} minutes`, ctx.cwd);
6170
+ tryPostPr7(prNumber, `\u231B kody waitForCi: timed out after ${timeoutMinutes} minutes`, ctx.cwd);
6043
6171
  };
6044
6172
  function fetchChecks(prNumber, cwd) {
6045
6173
  try {
6046
- const raw = execFileSync22("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
6174
+ const raw = execFileSync23("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
6047
6175
  encoding: "utf-8",
6048
6176
  timeout: API_TIMEOUT_MS10,
6049
6177
  cwd,
@@ -6079,7 +6207,7 @@ function numArg(args, key, fallback) {
6079
6207
  }
6080
6208
  return fallback;
6081
6209
  }
6082
- function tryPostPr6(prNumber, body, cwd) {
6210
+ function tryPostPr7(prNumber, body, cwd) {
6083
6211
  try {
6084
6212
  postPrReviewComment(prNumber, body, cwd);
6085
6213
  } catch {
@@ -6252,6 +6380,7 @@ var preflightScripts = {
6252
6380
  fixFlow,
6253
6381
  fixCiFlow,
6254
6382
  resolveFlow,
6383
+ revertFlow,
6255
6384
  reviewFlow,
6256
6385
  syncFlow,
6257
6386
  initFlow,
@@ -6312,7 +6441,8 @@ var postflightScripts = {
6312
6441
  notifyTerminal,
6313
6442
  recordOutcome,
6314
6443
  mergeReleasePr,
6315
- waitForCi
6444
+ waitForCi,
6445
+ markFlowSuccess
6316
6446
  };
6317
6447
  var allScriptNames = /* @__PURE__ */ new Set([
6318
6448
  ...Object.keys(preflightScripts),
@@ -6320,7 +6450,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
6320
6450
  ]);
6321
6451
 
6322
6452
  // src/tools.ts
6323
- import { execFileSync as execFileSync23 } from "child_process";
6453
+ import { execFileSync as execFileSync24 } from "child_process";
6324
6454
  function verifyCliTools(tools, cwd) {
6325
6455
  const out = [];
6326
6456
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -6353,7 +6483,7 @@ function verifyOne(tool, cwd) {
6353
6483
  }
6354
6484
  function runShell(cmd, cwd, timeoutMs = 3e4) {
6355
6485
  try {
6356
- execFileSync23("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
6486
+ execFileSync24("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
6357
6487
  return true;
6358
6488
  } catch {
6359
6489
  return false;
@@ -6601,7 +6731,17 @@ function finish(out) {
6601
6731
  `);
6602
6732
  return out;
6603
6733
  }
6604
- var SHELL_TIMEOUT_MS = 3e5;
6734
+ var DEFAULT_SHELL_TIMEOUT_MS = 3e5;
6735
+ function resolveShellTimeoutMs(entry) {
6736
+ if (typeof entry.timeoutSec === "number" && entry.timeoutSec > 0) {
6737
+ return Math.floor(entry.timeoutSec * 1e3);
6738
+ }
6739
+ const envSec = Number(process.env.KODY_SHELL_TIMEOUT_SEC);
6740
+ if (Number.isFinite(envSec) && envSec > 0) {
6741
+ return Math.floor(envSec * 1e3);
6742
+ }
6743
+ return DEFAULT_SHELL_TIMEOUT_MS;
6744
+ }
6605
6745
  function runShellEntry(entry, ctx, profile) {
6606
6746
  const shellName = entry.shell;
6607
6747
  const shellPath = path21.join(profile.dir, shellName);
@@ -6620,12 +6760,13 @@ function runShellEntry(entry, ctx, profile) {
6620
6760
  for (const [k, v] of flattenConfig(ctx.config)) {
6621
6761
  env[`KODY_CFG_${k}`] = v;
6622
6762
  }
6763
+ const timeoutMs = resolveShellTimeoutMs(entry);
6623
6764
  const r = spawnSync("bash", [shellPath, ...positional], {
6624
6765
  cwd: ctx.cwd,
6625
6766
  encoding: "utf-8",
6626
6767
  env,
6627
6768
  stdio: ["pipe", "pipe", "pipe"],
6628
- timeout: SHELL_TIMEOUT_MS
6769
+ timeout: timeoutMs
6629
6770
  });
6630
6771
  const stdout = r.stdout ?? "";
6631
6772
  const stderr = r.stderr ?? "";
@@ -6639,6 +6780,18 @@ function runShellEntry(entry, ctx, profile) {
6639
6780
  if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
6640
6781
  const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
6641
6782
  if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
6783
+ const timedOut = r.status === null && r.signal !== null;
6784
+ if (timedOut) {
6785
+ ctx.skipAgent = true;
6786
+ const seconds = Math.round(timeoutMs / 1e3);
6787
+ if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
6788
+ ctx.output.exitCode = 124;
6789
+ }
6790
+ if (!ctx.output.reason) {
6791
+ ctx.output.reason = `shell '${shellName}' timed out after ${seconds}s (signal=${r.signal})`;
6792
+ }
6793
+ return;
6794
+ }
6642
6795
  const exit = r.status ?? -1;
6643
6796
  if (exit !== 0) {
6644
6797
  ctx.skipAgent = true;
@@ -6762,7 +6915,7 @@ function detectPackageManager2(cwd) {
6762
6915
  }
6763
6916
  function shellOut(cmd, args, cwd, stream = true) {
6764
6917
  try {
6765
- execFileSync24(cmd, args, {
6918
+ execFileSync25(cmd, args, {
6766
6919
  cwd,
6767
6920
  stdio: stream ? "inherit" : "pipe",
6768
6921
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -6775,7 +6928,7 @@ function shellOut(cmd, args, cwd, stream = true) {
6775
6928
  }
6776
6929
  function isOnPath(bin) {
6777
6930
  try {
6778
- execFileSync24("which", [bin], { stdio: "pipe" });
6931
+ execFileSync25("which", [bin], { stdio: "pipe" });
6779
6932
  return true;
6780
6933
  } catch {
6781
6934
  return false;
@@ -6809,7 +6962,7 @@ function installLitellmIfNeeded(cwd) {
6809
6962
  } catch {
6810
6963
  }
6811
6964
  try {
6812
- execFileSync24("python3", ["-c", "import litellm"], { stdio: "pipe" });
6965
+ execFileSync25("python3", ["-c", "import litellm"], { stdio: "pipe" });
6813
6966
  process.stdout.write("\u2192 kody: litellm already installed\n");
6814
6967
  return 0;
6815
6968
  } catch {
@@ -6819,16 +6972,16 @@ function installLitellmIfNeeded(cwd) {
6819
6972
  }
6820
6973
  function configureGitIdentity(cwd) {
6821
6974
  try {
6822
- const name = execFileSync24("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
6975
+ const name = execFileSync25("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
6823
6976
  if (name) return;
6824
6977
  } catch {
6825
6978
  }
6826
6979
  try {
6827
- execFileSync24("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
6980
+ execFileSync25("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
6828
6981
  } catch {
6829
6982
  }
6830
6983
  try {
6831
- execFileSync24("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
6984
+ execFileSync25("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
6832
6985
  cwd,
6833
6986
  stdio: "pipe"
6834
6987
  });
@@ -7099,9 +7252,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
7099
7252
  if (paths.length === 0) return;
7100
7253
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
7101
7254
  try {
7102
- execFileSync25("git", ["add", ...paths], opts);
7103
- execFileSync25("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
7104
- execFileSync25("git", ["push", "--quiet", "origin", "HEAD"], opts);
7255
+ execFileSync26("git", ["add", ...paths], opts);
7256
+ execFileSync26("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
7257
+ execFileSync26("git", ["push", "--quiet", "origin", "HEAD"], opts);
7105
7258
  } catch (err) {
7106
7259
  const msg = err instanceof Error ? err.message : String(err);
7107
7260
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "revert",
3
+ "role": "primitive",
4
+ "describe": "Revert one or more commits on an existing PR branch via `git revert`. No agent — fully mechanical.",
5
+ "inputs": [
6
+ {
7
+ "name": "pr",
8
+ "flag": "--pr",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub PR number whose branch to revert commits on."
12
+ },
13
+ {
14
+ "name": "shas",
15
+ "flag": "--shas",
16
+ "type": "string",
17
+ "required": true,
18
+ "bindsCommentRest": true,
19
+ "describe": "One or more commit SHAs (whitespace-separated) to revert. Each must exist in the PR branch's history."
20
+ }
21
+ ],
22
+ "claudeCode": {
23
+ "model": "inherit",
24
+ "permissionMode": "acceptEdits",
25
+ "maxTurns": 0,
26
+ "maxThinkingTokens": null,
27
+ "systemPromptAppend": null,
28
+ "tools": [],
29
+ "hooks": [],
30
+ "skills": [],
31
+ "commands": [],
32
+ "subagents": [],
33
+ "plugins": [],
34
+ "mcpServers": []
35
+ },
36
+ "cliTools": [],
37
+ "scripts": {
38
+ "preflight": [
39
+ {
40
+ "script": "setLifecycleLabel",
41
+ "with": {
42
+ "label": "kody:reverting",
43
+ "color": "cccccc",
44
+ "description": "kody: reverting commits"
45
+ }
46
+ },
47
+ { "script": "revertFlow" },
48
+ { "shell": "revert.sh" }
49
+ ],
50
+ "postflight": [
51
+ { "script": "markFlowSuccess" },
52
+ { "script": "commitAndPush" },
53
+ { "script": "ensurePr" },
54
+ { "script": "postIssueComment" },
55
+ { "script": "writeRunSummary" }
56
+ ]
57
+ },
58
+ "output": {
59
+ "actionTypes": [
60
+ "REVERT_COMPLETED",
61
+ "REVERT_FAILED"
62
+ ]
63
+ }
64
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # revert: stage `git revert` of one or more commits on the PR branch.
4
+ #
5
+ # Runs as a preflight shell entry after revertFlow has validated inputs and
6
+ # checked out the branch. revertFlow:
7
+ # - Resolved every requested SHA to its full form and re-set
8
+ # ctx.args.shas to a whitespace-separated list (we read it via
9
+ # KODY_ARG_SHAS).
10
+ # - Set ctx.skipAgent=true and ctx.data.commitMessage already, so the
11
+ # agent never runs and commitAndPush will use that message.
12
+ #
13
+ # This script does only the staging — `--no-commit` so commitAndPush
14
+ # (postflight) makes the actual commit. That keeps kody's invariant
15
+ # intact (only commitAndPush commits) and means the message comes from
16
+ # revertFlow, not from git's auto-generated revert subject.
17
+ #
18
+ # Exits:
19
+ # 0 — staged successfully
20
+ # 1+ — git revert failed (executor surfaces stderr; postflight bails)
21
+
22
+ set -euo pipefail
23
+
24
+ shas="${KODY_ARG_SHAS:-}"
25
+ if [[ -z "$shas" ]]; then
26
+ echo "revert.sh: KODY_ARG_SHAS is empty (revertFlow should have set it)" >&2
27
+ exit 64
28
+ fi
29
+
30
+ # shellcheck disable=SC2086 # Intentional word-splitting on whitespace-separated SHAs.
31
+ git revert --no-commit --no-edit $shas
32
+
33
+ echo "revert.sh: staged revert of: $shas"
@@ -185,6 +185,14 @@ export interface ScriptEntry {
185
185
  * dispatcher script can be reused with different `next` targets.
186
186
  */
187
187
  with?: Record<string, string | number | boolean>
188
+ /**
189
+ * Optional shell-script timeout in seconds. Only honored on `shell` entries.
190
+ * Falls back to `KODY_SHELL_TIMEOUT_SEC` env var, then the 300s default.
191
+ * Long-running shells (release publish, large repo verify) should declare
192
+ * a higher value rather than relying on the default and getting SIGKILLed
193
+ * with an opaque "exited -1".
194
+ */
195
+ timeoutSec?: number
188
196
  }
189
197
 
190
198
  export interface OutputContract {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.50",
3
+ "version": "0.3.52",
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",