@kody-ade/kody-engine 0.4.31 → 0.4.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/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.4.31",
6
+ version: "0.4.33",
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",
@@ -5169,6 +5169,30 @@ var ensureLifecycleLabels = async (ctx) => {
5169
5169
  };
5170
5170
 
5171
5171
  // src/pr.ts
5172
+ function prMergeStatus(prNumber, cwd) {
5173
+ try {
5174
+ const out = gh(
5175
+ ["pr", "view", String(prNumber), "--json", "mergeable,mergeStateStatus"],
5176
+ { cwd }
5177
+ );
5178
+ const parsed = JSON.parse(out);
5179
+ const mergeable = parsed.mergeable ?? "UNKNOWN";
5180
+ const mergeStateStatus = parsed.mergeStateStatus ?? "UNKNOWN";
5181
+ return { status: classifyMergeStatus(mergeable, mergeStateStatus), mergeable, mergeStateStatus };
5182
+ } catch {
5183
+ return { status: "ERROR", mergeable: "", mergeStateStatus: "" };
5184
+ }
5185
+ }
5186
+ function classifyMergeStatus(mergeable, mergeStateStatus) {
5187
+ if (mergeable === "CONFLICTING") return "CONFLICTING";
5188
+ if (mergeable === "UNKNOWN") return "UNKNOWN";
5189
+ if (mergeable === "MERGEABLE") {
5190
+ if (mergeStateStatus === "CLEAN") return "MERGEABLE";
5191
+ if (mergeStateStatus === "DIRTY") return "CONFLICTING";
5192
+ return "BLOCKED";
5193
+ }
5194
+ return "UNKNOWN";
5195
+ }
5172
5196
  var TITLE_MAX = 72;
5173
5197
  function stripTitlePrefixes(raw) {
5174
5198
  let s = raw.trim();
@@ -5304,20 +5328,37 @@ function ensurePr(opts) {
5304
5328
  }
5305
5329
 
5306
5330
  // src/scripts/ensurePr.ts
5331
+ function setOutcome(ctx, outcome) {
5332
+ ctx.data.prResult = outcome;
5333
+ if (outcome.kind === "created" || outcome.kind === "updated") {
5334
+ ctx.output.prUrl = outcome.url;
5335
+ }
5336
+ }
5307
5337
  var ensurePr2 = async (ctx) => {
5308
5338
  if (ctx.skipAgent && ctx.output.exitCode !== void 0) {
5339
+ setOutcome(ctx, { kind: "skipped", reason: "preflight short-circuited (skipAgent)" });
5309
5340
  return;
5310
5341
  }
5311
5342
  const commitResult = ctx.data.commitResult;
5312
5343
  const hasCommits = Boolean(ctx.data.hasCommitsAhead);
5313
5344
  if (!commitResult?.committed && !hasCommits) {
5345
+ setOutcome(ctx, { kind: "skipped", reason: "no commits to ship" });
5314
5346
  return;
5315
5347
  }
5316
5348
  if (commitResult?.committed && commitResult.pushed === false) {
5349
+ setOutcome(ctx, { kind: "skipped", reason: "local commit succeeded but push failed" });
5350
+ return;
5351
+ }
5352
+ if (ctx.data.verifyOk === false) {
5353
+ const reason = `verify failed: ${ctx.data.verifyReason ?? "unknown"}`;
5354
+ setOutcome(ctx, { kind: "skipped", reason });
5317
5355
  return;
5318
5356
  }
5319
5357
  const branch = ctx.data.branch;
5320
- if (!branch) return;
5358
+ if (!branch) {
5359
+ setOutcome(ctx, { kind: "skipped", reason: "no branch context (ctx.data.branch missing)" });
5360
+ return;
5361
+ }
5321
5362
  const failureReason = computeFailureReason(ctx);
5322
5363
  const isFailure = failureReason.length > 0;
5323
5364
  const changedFiles = ctx.data.changedFiles ?? [];
@@ -5339,13 +5380,26 @@ var ensurePr2 = async (ctx) => {
5339
5380
  baseBranch,
5340
5381
  cwd: ctx.cwd
5341
5382
  });
5342
- ctx.output.prUrl = result.url;
5343
- ctx.data.prResult = result;
5383
+ if (!result.url || result.url.trim().length === 0) {
5384
+ const reason = `gh pr create returned empty URL (action=${result.action}); refusing to claim success`;
5385
+ ctx.data.prCrashReason = reason;
5386
+ ctx.output.exitCode = 4;
5387
+ ctx.output.reason = reason;
5388
+ setOutcome(ctx, { kind: "crashed", reason });
5389
+ return;
5390
+ }
5391
+ setOutcome(ctx, {
5392
+ kind: result.action === "created" ? "created" : "updated",
5393
+ url: result.url,
5394
+ number: result.number,
5395
+ draft: result.draft
5396
+ });
5344
5397
  } catch (err) {
5345
5398
  const reason = `PR creation failed: ${err instanceof Error ? err.message : String(err)}`;
5346
5399
  ctx.data.prCrashReason = reason;
5347
5400
  ctx.output.exitCode = 4;
5348
5401
  ctx.output.reason = reason;
5402
+ setOutcome(ctx, { kind: "crashed", reason });
5349
5403
  }
5350
5404
  };
5351
5405
  function computeFailureReason(ctx) {
@@ -7235,6 +7289,22 @@ var persistFlowState = async (ctx) => {
7235
7289
  }
7236
7290
  };
7237
7291
 
7292
+ // src/scripts/prOutcome.ts
7293
+ function readPrOutcome(data) {
7294
+ const raw = data.prResult;
7295
+ if (!raw || typeof raw !== "object") return null;
7296
+ const r = raw;
7297
+ switch (r.kind) {
7298
+ case "created":
7299
+ case "updated":
7300
+ case "skipped":
7301
+ case "crashed":
7302
+ return raw;
7303
+ default:
7304
+ return null;
7305
+ }
7306
+ }
7307
+
7238
7308
  // src/scripts/postIssueComment.ts
7239
7309
  var FAILED_LABEL_SPEC = {
7240
7310
  label: "kody:failed",
@@ -7248,8 +7318,7 @@ var postIssueComment2 = async (ctx) => {
7248
7318
  if (!targetType || !targetNumber) return;
7249
7319
  const commitResult = ctx.data.commitResult;
7250
7320
  const hasCommits = Boolean(ctx.data.hasCommitsAhead);
7251
- const prUrl = ctx.output.prUrl;
7252
- const prAction = ctx.data.prResult?.action;
7321
+ const prResult = readPrOutcome(ctx.data);
7253
7322
  if (!commitResult?.committed && !hasCommits) {
7254
7323
  const specific = computeFailureReason2(ctx);
7255
7324
  const reason = specific.length > 0 ? specific : "no changes to commit";
@@ -7267,18 +7336,17 @@ var postIssueComment2 = async (ctx) => {
7267
7336
  }
7268
7337
  const failureReason = computeFailureReason2(ctx);
7269
7338
  const isFailure = failureReason.length > 0;
7270
- const justPushedToExistingPr = prAction === "updated" && commitResult?.committed === true;
7271
- const successMsg = justPushedToExistingPr ? `\u2705 kody pushed to ${prUrl}` : prAction === "updated" ? `\u2139\uFE0F kody made no changes \u2014 PR: ${prUrl}` : `\u2705 kody PR opened: ${prUrl}`;
7272
7339
  const branch = ctx.data.branch;
7273
- const failurePrSuffix = computeFailureSuffix({
7274
- prUrl,
7275
- prAction,
7340
+ const msg = renderMessage({
7341
+ prResult,
7342
+ isFailure,
7343
+ failureReason,
7344
+ justPushedToExistingPr: prResult?.kind === "updated" && commitResult?.committed === true,
7276
7345
  branch,
7277
7346
  branchPushed: commitResult?.committed === true,
7278
7347
  githubOwner: ctx.config.github?.owner,
7279
7348
  githubRepo: ctx.config.github?.repo
7280
7349
  });
7281
- const msg = isFailure ? `\u26A0\uFE0F kody FAILED: ${truncate2(failureReason, 1500)}${failurePrSuffix}` : successMsg;
7282
7350
  postWith(targetType, targetNumber, msg, ctx.cwd);
7283
7351
  let exitCode = 0;
7284
7352
  const agentDone = Boolean(ctx.data.agentDone);
@@ -7302,12 +7370,29 @@ function markRunFailed(ctx) {
7302
7370
  }
7303
7371
  }
7304
7372
  function computeFailureSuffix(input) {
7305
- if (input.prUrl) {
7306
- return input.prAction === "updated" ? ` \u2014 PR: ${input.prUrl}` : ` \u2014 draft PR: ${input.prUrl}`;
7307
- }
7373
+ if (input.prResult?.kind === "created") return ` \u2014 draft PR: ${input.prResult.url}`;
7374
+ if (input.prResult?.kind === "updated") return ` \u2014 PR: ${input.prResult.url}`;
7308
7375
  if (!input.branchPushed || !input.branch || !input.githubOwner || !input.githubRepo) return "";
7309
7376
  return ` \u2014 branch: https://github.com/${input.githubOwner}/${input.githubRepo}/tree/${input.branch}`;
7310
7377
  }
7378
+ function renderMessage(input) {
7379
+ const suffix = computeFailureSuffix(input);
7380
+ if (input.isFailure) {
7381
+ return `\u26A0\uFE0F kody FAILED: ${truncate2(input.failureReason, 1500)}${suffix}`;
7382
+ }
7383
+ switch (input.prResult?.kind) {
7384
+ case "created":
7385
+ return `\u2705 kody PR opened: ${input.prResult.url}`;
7386
+ case "updated":
7387
+ return input.justPushedToExistingPr ? `\u2705 kody pushed to ${input.prResult.url}` : `\u2139\uFE0F kody made no changes \u2014 PR: ${input.prResult.url}`;
7388
+ case "skipped":
7389
+ return `\u26A0\uFE0F kody finished but did not open a PR \u2014 ${input.prResult.reason}${suffix}`;
7390
+ case "crashed":
7391
+ return `\u26A0\uFE0F kody PR step crashed: ${truncate2(input.prResult.reason, 1500)}${suffix}`;
7392
+ case void 0:
7393
+ return `\u26A0\uFE0F kody finished but PR step did not run${suffix}`;
7394
+ }
7395
+ }
7311
7396
  function computeFailureReason2(ctx) {
7312
7397
  const misses = ctx.data.coverageMisses ?? [];
7313
7398
  if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
@@ -7560,12 +7645,35 @@ var resolveFlow = async (ctx) => {
7560
7645
  ctx.data.pr = pr;
7561
7646
  ctx.data.commentTargetType = "pr";
7562
7647
  ctx.data.commentTargetNumber = prNumber;
7563
- checkoutPrBranch(prNumber, ctx.cwd);
7564
- ctx.data.branch = getCurrentBranch(ctx.cwd);
7565
7648
  const baseBranch = pr.baseRefName || ctx.config.git.defaultBranch;
7566
7649
  ctx.data.baseBranch = baseBranch;
7650
+ const ghStatus = prMergeStatus(prNumber, ctx.cwd);
7651
+ if (ghStatus.status === "MERGEABLE") {
7652
+ ctx.output.exitCode = 0;
7653
+ ctx.output.reason = `PR #${prNumber} is mergeable (no conflicts) \u2014 nothing to resolve`;
7654
+ ctx.skipAgent = true;
7655
+ tryPostPr3(prNumber, `\u2139\uFE0F kody resolve: ${ctx.output.reason}`, ctx.cwd);
7656
+ return;
7657
+ }
7658
+ if (ghStatus.status === "BLOCKED") {
7659
+ ctx.output.exitCode = 0;
7660
+ ctx.output.reason = `PR #${prNumber} is mergeable but blocked by checks/reviews (mergeStateStatus=${ghStatus.mergeStateStatus}) \u2014 nothing for resolve to do`;
7661
+ ctx.skipAgent = true;
7662
+ tryPostPr3(prNumber, `\u2139\uFE0F kody resolve: ${ctx.output.reason}`, ctx.cwd);
7663
+ return;
7664
+ }
7665
+ checkoutPrBranch(prNumber, ctx.cwd);
7666
+ ctx.data.branch = getCurrentBranch(ctx.cwd);
7567
7667
  const mergeStatus = mergeBase(baseBranch, ctx.cwd);
7568
7668
  if (mergeStatus === "clean") {
7669
+ if (ghStatus.status === "CONFLICTING") {
7670
+ const pushed = pushEmptyCommit(ctx.data.branch, ctx.cwd);
7671
+ ctx.output.exitCode = 0;
7672
+ ctx.output.reason = pushed ? `local merge clean despite GitHub reporting CONFLICTING \u2014 pushed empty commit to force re-evaluation` : `local merge clean despite GitHub reporting CONFLICTING \u2014 couldn't refresh GitHub cache (push failed)`;
7673
+ ctx.skipAgent = true;
7674
+ tryPostPr3(prNumber, `\u2139\uFE0F kody resolve: ${ctx.output.reason}`, ctx.cwd);
7675
+ return;
7676
+ }
7569
7677
  ctx.output.exitCode = 0;
7570
7678
  ctx.output.reason = `already up to date with origin/${baseBranch} \u2014 nothing to resolve`;
7571
7679
  ctx.skipAgent = true;
@@ -7652,6 +7760,24 @@ function tryPostPr3(prNumber, body, cwd) {
7652
7760
  } catch {
7653
7761
  }
7654
7762
  }
7763
+ function pushEmptyCommit(branch, cwd) {
7764
+ const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
7765
+ try {
7766
+ execFileSync22(
7767
+ "git",
7768
+ ["commit", "--allow-empty", "-m", "chore: kody resolve refresh \u2014 empty commit to recompute mergeable status"],
7769
+ { cwd, env, stdio: ["ignore", "pipe", "pipe"] }
7770
+ );
7771
+ execFileSync22("git", ["push", "-u", "origin", branch], {
7772
+ cwd,
7773
+ env,
7774
+ stdio: ["ignore", "pipe", "pipe"]
7775
+ });
7776
+ return true;
7777
+ } catch {
7778
+ return false;
7779
+ }
7780
+ }
7655
7781
 
7656
7782
  // src/deployments.ts
7657
7783
  function findPreviewDeploymentUrl(prNumber, cwd) {
@@ -65,7 +65,7 @@
65
65
  { "script": "checkCoverageWithRetry" },
66
66
  { "script": "abortUnfinishedGitOps" },
67
67
  { "script": "commitAndPush" },
68
- { "script": "ensurePr", "runWhen": { "data.verifyOk": true } },
68
+ { "script": "ensurePr" },
69
69
  { "script": "postIssueComment" },
70
70
  { "script": "writeRunSummary" },
71
71
  { "script": "saveTaskState" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.31",
3
+ "version": "0.4.33",
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",