@kody-ade/kody-engine 0.3.31 → 0.3.32

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.31",
6
+ version: "0.3.32",
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 execFileSync22 } from "child_process";
53
+ import { execFileSync as execFileSync23 } from "child_process";
54
54
  import * as fs22 from "fs";
55
55
  import * as path19 from "path";
56
56
 
@@ -577,7 +577,7 @@ async function emit(sink, type, sessionId, suffix, payload) {
577
577
  }
578
578
 
579
579
  // src/kody-cli.ts
580
- import { execFileSync as execFileSync21 } from "child_process";
580
+ import { execFileSync as execFileSync22 } from "child_process";
581
581
  import * as fs21 from "fs";
582
582
  import * as path18 from "path";
583
583
 
@@ -5145,6 +5145,135 @@ var verify = async (ctx) => {
5145
5145
  }
5146
5146
  };
5147
5147
 
5148
+ // src/scripts/waitForCi.ts
5149
+ import { execFileSync as execFileSync20 } from "child_process";
5150
+ var API_TIMEOUT_MS9 = 3e4;
5151
+ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5152
+ const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
5153
+ const pollSeconds = numArg(args, "pollSeconds", 30);
5154
+ const initialWaitSeconds = numArg(args, "initialWaitSeconds", 15);
5155
+ const maxFixCiAttempts = numArg(args, "maxFixCiAttempts", 3);
5156
+ const state = ctx.data.taskState;
5157
+ const prUrl = state?.core.prUrl;
5158
+ const prNumber = prUrl ? parsePrNumber(prUrl) : null;
5159
+ if (!prNumber) {
5160
+ ctx.data.action = mkAction("CI_GIVEUP", { reason: "no PR url in state \u2014 nothing to wait for" });
5161
+ return;
5162
+ }
5163
+ const fixCiAttempts = state?.core.attempts?.["fix-ci"] ?? 0;
5164
+ await sleep(initialWaitSeconds * 1e3);
5165
+ const deadline = Date.now() + timeoutMinutes * 6e4;
5166
+ let lastSummary = "";
5167
+ while (Date.now() < deadline) {
5168
+ const rows = fetchChecks(prNumber, ctx.cwd);
5169
+ if (rows === null) {
5170
+ await sleep(pollSeconds * 1e3);
5171
+ continue;
5172
+ }
5173
+ if (rows.length === 0) {
5174
+ await sleep(pollSeconds * 1e3);
5175
+ continue;
5176
+ }
5177
+ const summary = summarize(rows);
5178
+ if (summary !== lastSummary) {
5179
+ lastSummary = summary;
5180
+ tryPostPr6(prNumber, `\u23F3 kody waitForCi: ${summary}`, ctx.cwd);
5181
+ }
5182
+ const failed = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
5183
+ const pending = rows.filter((r) => r.bucket === "pending");
5184
+ if (failed.length > 0) {
5185
+ const detail = failed.slice(0, 5).map((r) => `${r.workflow ?? "?"} / ${r.name ?? "?"}${r.link ? ` (${r.link})` : ""}`).join("; ");
5186
+ if (fixCiAttempts >= maxFixCiAttempts) {
5187
+ ctx.data.action = mkAction("CI_GIVEUP", {
5188
+ reason: `fix-ci attempts (${fixCiAttempts}) hit cap (${maxFixCiAttempts})`,
5189
+ failedChecks: detail,
5190
+ prUrl
5191
+ });
5192
+ tryPostPr6(
5193
+ prNumber,
5194
+ `\u{1F6D1} kody waitForCi: giving up after ${fixCiAttempts} fix-ci attempts. Failed: ${detail}`,
5195
+ ctx.cwd
5196
+ );
5197
+ } else {
5198
+ ctx.data.action = mkAction("CI_FAILED", {
5199
+ failedChecks: detail,
5200
+ attempt: fixCiAttempts + 1,
5201
+ maxAttempts: maxFixCiAttempts,
5202
+ prUrl
5203
+ });
5204
+ tryPostPr6(
5205
+ prNumber,
5206
+ `\u274C kody waitForCi: CI failed (attempt ${fixCiAttempts + 1}/${maxFixCiAttempts}). Dispatching fix-ci. Failed: ${detail}`,
5207
+ ctx.cwd
5208
+ );
5209
+ }
5210
+ return;
5211
+ }
5212
+ if (pending.length === 0) {
5213
+ ctx.data.action = mkAction("CI_PASSED", { checks: rows.length, prUrl });
5214
+ tryPostPr6(prNumber, `\u2705 kody waitForCi: all ${rows.length} checks green on PR #${prNumber}`, ctx.cwd);
5215
+ return;
5216
+ }
5217
+ await sleep(pollSeconds * 1e3);
5218
+ }
5219
+ ctx.data.action = mkAction("CI_TIMEOUT", {
5220
+ reason: `CI did not complete within ${timeoutMinutes} minutes`,
5221
+ prUrl
5222
+ });
5223
+ tryPostPr6(prNumber, `\u231B kody waitForCi: timed out after ${timeoutMinutes} minutes`, ctx.cwd);
5224
+ };
5225
+ function fetchChecks(prNumber, cwd) {
5226
+ try {
5227
+ const raw = execFileSync20(
5228
+ "gh",
5229
+ ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"],
5230
+ {
5231
+ encoding: "utf-8",
5232
+ timeout: API_TIMEOUT_MS9,
5233
+ cwd,
5234
+ stdio: ["ignore", "pipe", "pipe"]
5235
+ }
5236
+ );
5237
+ const parsed = JSON.parse(raw);
5238
+ return Array.isArray(parsed) ? parsed : [];
5239
+ } catch (err) {
5240
+ process.stderr.write(
5241
+ `[kody waitForCi] gh pr checks #${prNumber} failed: ${err instanceof Error ? err.message : String(err)}
5242
+ `
5243
+ );
5244
+ return null;
5245
+ }
5246
+ }
5247
+ function summarize(rows) {
5248
+ const counts = {};
5249
+ for (const r of rows) {
5250
+ const k = r.bucket ?? "unknown";
5251
+ counts[k] = (counts[k] ?? 0) + 1;
5252
+ }
5253
+ return Object.entries(counts).map(([k, v]) => `${k}:${v}`).join(" ");
5254
+ }
5255
+ function mkAction(type, payload) {
5256
+ return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5257
+ }
5258
+ function numArg(args, key, fallback) {
5259
+ const v = args?.[key];
5260
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) return v;
5261
+ if (typeof v === "string") {
5262
+ const n = Number(v);
5263
+ if (Number.isFinite(n) && n >= 0) return n;
5264
+ }
5265
+ return fallback;
5266
+ }
5267
+ function tryPostPr6(prNumber, body, cwd) {
5268
+ try {
5269
+ postPrReviewComment(prNumber, body, cwd);
5270
+ } catch {
5271
+ }
5272
+ }
5273
+ function sleep(ms) {
5274
+ return new Promise((res) => setTimeout(res, ms));
5275
+ }
5276
+
5148
5277
  // src/scripts/watchStalePrsFlow.ts
5149
5278
  function readWatchConfig(ctx) {
5150
5279
  const cfg = ctx.config.watch;
@@ -5336,7 +5465,8 @@ var postflightScripts = {
5336
5465
  postClassification,
5337
5466
  notifyTerminal,
5338
5467
  recordOutcome,
5339
- mergeReleasePr
5468
+ mergeReleasePr,
5469
+ waitForCi
5340
5470
  };
5341
5471
  var allScriptNames = /* @__PURE__ */ new Set([
5342
5472
  ...Object.keys(preflightScripts),
@@ -5344,7 +5474,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
5344
5474
  ]);
5345
5475
 
5346
5476
  // src/tools.ts
5347
- import { execFileSync as execFileSync20 } from "child_process";
5477
+ import { execFileSync as execFileSync21 } from "child_process";
5348
5478
  function verifyCliTools(tools, cwd) {
5349
5479
  const out = [];
5350
5480
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -5377,7 +5507,7 @@ function verifyOne(tool, cwd) {
5377
5507
  }
5378
5508
  function runShell(cmd, cwd, timeoutMs = 3e4) {
5379
5509
  try {
5380
- execFileSync20("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5510
+ execFileSync21("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
5381
5511
  return true;
5382
5512
  } catch {
5383
5513
  return false;
@@ -5784,7 +5914,7 @@ function detectPackageManager2(cwd) {
5784
5914
  }
5785
5915
  function shellOut(cmd, args, cwd, stream = true) {
5786
5916
  try {
5787
- execFileSync21(cmd, args, {
5917
+ execFileSync22(cmd, args, {
5788
5918
  cwd,
5789
5919
  stdio: stream ? "inherit" : "pipe",
5790
5920
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -5797,7 +5927,7 @@ function shellOut(cmd, args, cwd, stream = true) {
5797
5927
  }
5798
5928
  function isOnPath(bin) {
5799
5929
  try {
5800
- execFileSync21("which", [bin], { stdio: "pipe" });
5930
+ execFileSync22("which", [bin], { stdio: "pipe" });
5801
5931
  return true;
5802
5932
  } catch {
5803
5933
  return false;
@@ -5831,7 +5961,7 @@ function installLitellmIfNeeded(cwd) {
5831
5961
  } catch {
5832
5962
  }
5833
5963
  try {
5834
- execFileSync21("python3", ["-c", "import litellm"], { stdio: "pipe" });
5964
+ execFileSync22("python3", ["-c", "import litellm"], { stdio: "pipe" });
5835
5965
  process.stdout.write("\u2192 kody: litellm already installed\n");
5836
5966
  return 0;
5837
5967
  } catch {
@@ -5841,16 +5971,16 @@ function installLitellmIfNeeded(cwd) {
5841
5971
  }
5842
5972
  function configureGitIdentity(cwd) {
5843
5973
  try {
5844
- const name = execFileSync21("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5974
+ const name = execFileSync22("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
5845
5975
  if (name) return;
5846
5976
  } catch {
5847
5977
  }
5848
5978
  try {
5849
- execFileSync21("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5979
+ execFileSync22("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
5850
5980
  } catch {
5851
5981
  }
5852
5982
  try {
5853
- execFileSync21("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5983
+ execFileSync22("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
5854
5984
  cwd,
5855
5985
  stdio: "pipe"
5856
5986
  });
@@ -6032,9 +6162,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
6032
6162
  if (paths.length === 0) return;
6033
6163
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
6034
6164
  try {
6035
- execFileSync22("git", ["add", ...paths], opts);
6036
- execFileSync22("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
6037
- execFileSync22("git", ["push", "--quiet", "origin", "HEAD"], opts);
6165
+ execFileSync23("git", ["add", ...paths], opts);
6166
+ execFileSync23("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
6167
+ execFileSync23("git", ["push", "--quiet", "origin", "HEAD"], opts);
6038
6168
  } catch (err) {
6039
6169
  const msg = err instanceof Error ? err.message : String(err);
6040
6170
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -62,13 +62,24 @@
62
62
  { "script": "dispatch", "with": { "next": "release-deploy", "target": "issue" },
63
63
  "runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_PUBLISH_COMPLETED" } },
64
64
 
65
+ { "script": "waitForCi",
66
+ "with": { "timeoutMinutes": 30, "pollSeconds": 30, "initialWaitSeconds": 15, "maxFixCiAttempts": 3 },
67
+ "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_DEPLOY_COMPLETED", "FIX_CI_COMPLETED"] } },
68
+
69
+ { "script": "dispatch", "with": { "next": "fix-ci", "target": "pr" },
70
+ "runWhen": { "data.action.type": "CI_FAILED" } },
71
+
65
72
  { "script": "finishFlow",
66
73
  "with": { "reason": "release-completed", "label": "kody:done", "color": "0e8a16", "description": "kody: release complete" },
67
- "runWhen": { "data.taskState.core.lastOutcome.type": "RELEASE_DEPLOY_COMPLETED" } },
74
+ "runWhen": { "data.action.type": "CI_PASSED" } },
75
+
76
+ { "script": "finishFlow",
77
+ "with": { "reason": "release-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: release flow failed" },
78
+ "runWhen": { "data.action.type": ["CI_GIVEUP", "CI_TIMEOUT"] } },
68
79
 
69
80
  { "script": "finishFlow",
70
81
  "with": { "reason": "release-failed", "label": "kody:failed", "color": "e11d21", "description": "kody: release flow failed" },
71
- "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_PREPARE_FAILED", "RELEASE_MERGE_FAILED", "RELEASE_PUBLISH_FAILED", "RELEASE_DEPLOY_FAILED"] } },
82
+ "runWhen": { "data.taskState.core.lastOutcome.type": ["RELEASE_PREPARE_FAILED", "RELEASE_MERGE_FAILED", "RELEASE_PUBLISH_FAILED", "RELEASE_DEPLOY_FAILED", "FIX_CI_FAILED"] } },
72
83
 
73
84
  { "script": "persistFlowState" }
74
85
  ]
@@ -74,6 +74,10 @@ existing=$(gh pr list --head "$default_branch" --base "$release_branch" --state
74
74
  | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data[0]["url"] if data else "")' 2>/dev/null \
75
75
  || echo "")
76
76
 
77
+ # Hoisted so the kody-release-pr marker write below also runs in the
78
+ # reuse-existing-PR path.
79
+ issue_arg="${KODY_ARG_ISSUE:-}"
80
+
77
81
  if [[ -n "$existing" ]]; then
78
82
  echo " reusing existing deploy PR: ${existing}"
79
83
  pr_url="$existing"
@@ -81,7 +85,6 @@ else
81
85
  # Same Tracking-Issue marker as release-prepare — non-closing reference
82
86
  # so the originating release issue stays open through the deploy step
83
87
  # while the Kody Dashboard can still link this PR to the task for preview.
84
- issue_arg="${KODY_ARG_ISSUE:-}"
85
88
  tracking_line=""
86
89
  if [[ "$issue_arg" =~ ^[0-9]+$ && "$issue_arg" != "0" ]]; then
87
90
  tracking_line=$'\n\nTracking-Issue: #'"${issue_arg}"
@@ -102,6 +105,24 @@ if [[ -z "$pr_url" ]]; then
102
105
  exit 1
103
106
  fi
104
107
 
108
+ # Persist the deploy-PR marker on the originating issue body. Mirrors the
109
+ # release-prepare path — the issue body is owned by the orchestrator, so
110
+ # this signal survives any @kody fix that overwrites the PR body. The
111
+ # marker replaces the prepare-PR ref so the dashboard pivots to the deploy
112
+ # PR (the now-current task) automatically.
113
+ if [[ "${issue_arg:-}" =~ ^[0-9]+$ && "${issue_arg:-0}" != "0" ]]; then
114
+ pr_number="${pr_url##*/}"
115
+ if [[ "$pr_number" =~ ^[0-9]+$ ]]; then
116
+ cur_body=$(gh issue view "$issue_arg" --json body -q .body 2>/dev/null || echo "")
117
+ cleaned_body=$(printf '%s' "$cur_body" | sed -E '/<!-- kody-release-pr:[^>]*-->/d')
118
+ {
119
+ printf '%s' "$cleaned_body"
120
+ printf '\n\n<!-- kody-release-pr: #%s -->\n' "$pr_number"
121
+ } | gh issue edit "$issue_arg" --body-file - >/dev/null 2>&1 || \
122
+ echo "[kody release-deploy] WARN: failed to write kody-release-pr marker to issue #${issue_arg}"
123
+ fi
124
+ fi
125
+
105
126
  echo "RELEASE_DEPLOY_PR=${pr_url}"
106
127
  echo "KODY_PR_URL=${pr_url}"
107
128
 
@@ -333,6 +333,24 @@ if [[ -z "$pr_url" ]]; then
333
333
  fail "release prepare: gh pr create returned empty URL" 4
334
334
  fi
335
335
 
336
+ # Persist a release-PR marker on the originating issue body so the Kody
337
+ # Dashboard can link the issue → PR even if @kody fix later overwrites the
338
+ # PR body. Idempotent: any existing marker line is stripped before append,
339
+ # so re-runs replace the previous PR ref. The issue body is owned by the
340
+ # orchestrator (no @kody fix touches it), so this signal is durable.
341
+ if [[ "${issue_arg:-}" =~ ^[0-9]+$ && "${issue_arg:-0}" != "0" ]]; then
342
+ pr_number="${pr_url##*/}"
343
+ if [[ "$pr_number" =~ ^[0-9]+$ ]]; then
344
+ cur_body=$(gh issue view "$issue_arg" --json body -q .body 2>/dev/null || echo "")
345
+ cleaned_body=$(printf '%s' "$cur_body" | sed -E '/<!-- kody-release-pr:[^>]*-->/d')
346
+ {
347
+ printf '%s' "$cleaned_body"
348
+ printf '\n\n<!-- kody-release-pr: #%s -->\n' "$pr_number"
349
+ } | gh issue edit "$issue_arg" --body-file - >/dev/null 2>&1 || \
350
+ echo "[kody release-prepare] WARN: failed to write kody-release-pr marker to issue #${issue_arg}"
351
+ fi
352
+ fi
353
+
336
354
  echo "RELEASE_PR=${pr_url}"
337
355
  echo "KODY_PR_URL=${pr_url}"
338
356
  echo "KODY_REASON=opened release PR for ${tag}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.31",
3
+ "version": "0.3.32",
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",