@kody-ade/kody-engine 0.3.9 → 0.3.10

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.9",
6
+ version: "0.3.10",
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",
@@ -4327,6 +4327,24 @@ function lastReleaseTag(cwd) {
4327
4327
  return null;
4328
4328
  }
4329
4329
  }
4330
+ function remoteBranchExists(branch, cwd) {
4331
+ try {
4332
+ const out = git3(["ls-remote", "--heads", "origin", branch], cwd, 3e4);
4333
+ return out.length > 0;
4334
+ } catch {
4335
+ return false;
4336
+ }
4337
+ }
4338
+ function findOpenPrForBranch(branch, cwd) {
4339
+ try {
4340
+ const out = gh2(["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--limit", "1"], { cwd });
4341
+ const parsed = JSON.parse(out || "[]");
4342
+ const first = parsed[0];
4343
+ return first?.url ?? null;
4344
+ } catch {
4345
+ return null;
4346
+ }
4347
+ }
4330
4348
  function runShell(cmd, cwd, timeoutMs) {
4331
4349
  const r = spawnSync(cmd, {
4332
4350
  cwd,
@@ -4341,6 +4359,7 @@ var releaseFlow = async (ctx) => {
4341
4359
  const mode = ctx.args.mode ?? "prepare";
4342
4360
  const bump = ctx.args.bump ?? "patch";
4343
4361
  const dryRun = ctx.args["dry-run"] === true || ctx.args.dryRun === true;
4362
+ const prefer = ctx.args.prefer ?? void 0;
4344
4363
  const issueNumber = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
4345
4364
  const cwd = ctx.cwd;
4346
4365
  const releaseCfg = ctx.config.release ?? {};
@@ -4348,7 +4367,7 @@ var releaseFlow = async (ctx) => {
4348
4367
  const timeoutMs = releaseCfg.timeoutMs ?? 6e5;
4349
4368
  ctx.skipAgent = true;
4350
4369
  if (mode === "prepare") {
4351
- await runPrepare({ cwd, bump, dryRun, versionFiles, ctx });
4370
+ await runPrepare({ cwd, bump, dryRun, prefer, versionFiles, ctx });
4352
4371
  } else if (mode === "finalize") {
4353
4372
  await runFinalize({ cwd, dryRun, timeoutMs, releaseCfg, ctx });
4354
4373
  } else {
@@ -4378,7 +4397,7 @@ function buildIssueNotice(mode, dryRun, ctx) {
4378
4397
  return `\u2705 kody ${label} complete`;
4379
4398
  }
4380
4399
  async function runPrepare(args) {
4381
- const { cwd, bump, dryRun, versionFiles, ctx } = args;
4400
+ const { cwd, bump, dryRun, prefer, versionFiles, ctx } = args;
4382
4401
  const pkgPath = path17.join(cwd, "package.json");
4383
4402
  if (!fs19.existsSync(pkgPath)) {
4384
4403
  ctx.output.exitCode = 99;
@@ -4398,11 +4417,35 @@ async function runPrepare(args) {
4398
4417
  `);
4399
4418
  if (dryRun) {
4400
4419
  ctx.output.exitCode = 0;
4401
- ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}`;
4420
+ ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}${prefer ? ` (--prefer ${prefer})` : ""}`;
4402
4421
  process.stdout.write(`RELEASE_PLAN=bump=${newVersion} tag=${tag}
4403
4422
  `);
4404
4423
  return;
4405
4424
  }
4425
+ const releaseBranch = `release/${tag}`;
4426
+ const collides = remoteBranchExists(releaseBranch, cwd);
4427
+ if (collides) {
4428
+ if (prefer === "theirs") {
4429
+ const existingPr = findOpenPrForBranch(releaseBranch, cwd);
4430
+ if (existingPr) {
4431
+ process.stdout.write(` reusing existing PR (--prefer theirs): ${existingPr}
4432
+ `);
4433
+ ctx.output.prUrl = existingPr;
4434
+ ctx.output.exitCode = 0;
4435
+ return;
4436
+ }
4437
+ ctx.output.exitCode = 4;
4438
+ ctx.output.reason = `release prepare --prefer theirs: ${releaseBranch} exists on remote but has no open PR \u2014 nothing to reuse`;
4439
+ return;
4440
+ }
4441
+ if (prefer !== "ours") {
4442
+ ctx.output.exitCode = 4;
4443
+ ctx.output.reason = `release prepare: branch ${releaseBranch} already exists on remote. Use --prefer ours to force-push, or --prefer theirs to reuse the existing PR.`;
4444
+ return;
4445
+ }
4446
+ process.stdout.write(` branch ${releaseBranch} exists on remote \u2014 will force-push (--prefer ours)
4447
+ `);
4448
+ }
4406
4449
  const touched = [];
4407
4450
  for (const f of versionFiles) {
4408
4451
  if (updateVersionInFile(f, newVersion, cwd)) touched.push(f);
@@ -4418,12 +4461,12 @@ async function runPrepare(args) {
4418
4461
  prependChangelog(cwd, entry);
4419
4462
  process.stdout.write(` wrote CHANGELOG.md
4420
4463
  `);
4421
- const releaseBranch = `release/${tag}`;
4422
4464
  try {
4423
4465
  git3(["checkout", "-b", releaseBranch], cwd);
4424
4466
  for (const f of [...touched, "CHANGELOG.md"]) git3(["add", "--", f], cwd);
4425
4467
  git3(["commit", "--no-gpg-sign", "-m", `chore: release ${tag}`], cwd);
4426
- git3(["push", "-u", "origin", releaseBranch], cwd, 12e4);
4468
+ const pushArgs = collides && prefer === "ours" ? ["push", "-u", "--force-with-lease", "origin", releaseBranch] : ["push", "-u", "origin", releaseBranch];
4469
+ git3(pushArgs, cwd, 12e4);
4427
4470
  } catch (err) {
4428
4471
  const msg = err instanceof Error ? err.message : String(err);
4429
4472
  ctx.output.exitCode = 4;
@@ -4442,16 +4485,23 @@ ${rawEntry}
4442
4485
 
4443
4486
  Merge this and then run \`kody release --mode finalize\`.`;
4444
4487
  let prUrl = "";
4445
- try {
4446
- prUrl = gh2(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
4447
- input: body,
4448
- cwd
4449
- }).trim();
4450
- } catch (err) {
4451
- const msg = err instanceof Error ? err.message : String(err);
4452
- ctx.output.exitCode = 4;
4453
- ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
4454
- return;
4488
+ const preexistingPr = collides && prefer === "ours" ? findOpenPrForBranch(releaseBranch, cwd) : null;
4489
+ if (preexistingPr) {
4490
+ process.stdout.write(` PR already open for ${releaseBranch}: ${preexistingPr}
4491
+ `);
4492
+ prUrl = preexistingPr;
4493
+ } else {
4494
+ try {
4495
+ prUrl = gh2(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
4496
+ input: body,
4497
+ cwd
4498
+ }).trim();
4499
+ } catch (err) {
4500
+ const msg = err instanceof Error ? err.message : String(err);
4501
+ ctx.output.exitCode = 4;
4502
+ ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
4503
+ return;
4504
+ }
4455
4505
  }
4456
4506
  ctx.output.prUrl = prUrl;
4457
4507
  ctx.output.exitCode = 0;
@@ -4681,6 +4731,7 @@ var resolveFlow = async (ctx) => {
4681
4731
  }
4682
4732
  ctx.data.conflictedFiles = conflictedFiles;
4683
4733
  ctx.data.conflictMarkersPreview = getConflictMarkersPreview(conflictedFiles, ctx.cwd);
4734
+ ctx.data.preferBlock = buildPreferBlock(ctx.args.prefer, baseBranch);
4684
4735
  const runUrl = getRunUrl();
4685
4736
  const runSuffix = runUrl ? `, run ${runUrl}` : "";
4686
4737
  tryPostPr3(
@@ -4689,6 +4740,23 @@ var resolveFlow = async (ctx) => {
4689
4740
  ctx.cwd
4690
4741
  );
4691
4742
  };
4743
+ function buildPreferBlock(prefer, baseBranch) {
4744
+ if (prefer !== "ours" && prefer !== "theirs") return "";
4745
+ const keepSide = prefer === "ours" ? "HEAD (this PR branch)" : `origin/${baseBranch} (base branch)`;
4746
+ const keepMarkers = prefer === "ours" ? "content between `<<<<<<< HEAD` and `=======`" : `content between \`=======\` and \`>>>>>>> origin/${baseBranch}\``;
4747
+ const dropSide = prefer === "ours" ? `origin/${baseBranch}` : "HEAD";
4748
+ return [
4749
+ "# Conflict resolution directive (AUTHORITATIVE \u2014 overrides defaults below)",
4750
+ "",
4751
+ `The user requested \`--prefer ${prefer}\`. For **every** conflict in **every** file:`,
4752
+ "",
4753
+ `- Keep the **${prefer}** side: ${keepSide} \u2014 ${keepMarkers}.`,
4754
+ `- Discard the **${prefer === "ours" ? "theirs" : "ours"}** side (from ${dropSide}) entirely.`,
4755
+ "- Remove all `<<<<<<<`, `=======`, `>>>>>>>` markers.",
4756
+ "- Do NOT attempt to merge the two sides or apply judgement.",
4757
+ ""
4758
+ ].join("\n");
4759
+ }
4692
4760
  function getConflictedFiles(cwd) {
4693
4761
  try {
4694
4762
  const out = execFileSync17("git", ["diff", "--name-only", "--diff-filter=U"], {
@@ -40,6 +40,17 @@
40
40
  "type": "int",
41
41
  "required": false,
42
42
  "describe": "Issue number to post success/failure follow-up on. Auto-populated by dispatch when triggered via @kody comment."
43
+ },
44
+ {
45
+ "name": "prefer",
46
+ "flag": "--prefer",
47
+ "type": "enum",
48
+ "values": [
49
+ "ours",
50
+ "theirs"
51
+ ],
52
+ "required": false,
53
+ "describe": "On release/vX.Y.Z branch collision (prepare mode): 'ours' force-pushes over the remote branch; 'theirs' reuses the existing branch and its open PR. Default (unset): refuse on non-ff."
43
54
  }
44
55
  ],
45
56
  "claudeCode": {
@@ -9,6 +9,14 @@
9
9
  "type": "int",
10
10
  "required": true,
11
11
  "describe": "GitHub PR number whose branch has conflicts with the default branch."
12
+ },
13
+ {
14
+ "name": "prefer",
15
+ "flag": "--prefer",
16
+ "type": "enum",
17
+ "values": ["ours", "theirs"],
18
+ "required": false,
19
+ "describe": "Force one side for every conflict: 'ours' = PR branch (HEAD), 'theirs' = base branch. Omit to let the agent merge by judgement."
12
20
  }
13
21
  ],
14
22
  "claudeCode": {
@@ -9,13 +9,13 @@ You are Kody, an autonomous engineer. A `git merge origin/{{baseBranch}}` into P
9
9
 
10
10
  {{conflictedFiles}}
11
11
 
12
- {{conventionsBlock}}{{toolsUsage}}# Working-tree conflict markers (truncated)
12
+ {{preferBlock}}{{conventionsBlock}}{{toolsUsage}}# Working-tree conflict markers (truncated)
13
13
 
14
14
  {{conflictMarkersPreview}}
15
15
 
16
16
  # Required steps
17
17
  1. For each conflicted file: read it, understand both sides of the `<<<<<<<` / `=======` / `>>>>>>>` markers, and produce the correct merged content. Remove all conflict markers.
18
- 2. Preserve the PR's intent (the HEAD side) unless `origin/{{baseBranch}}` made a change that should be preserved (e.g. security fix, renamed API). Use judgement.
18
+ 2. If a conflict resolution directive is given above, follow it exactly — take the specified side for every conflict, no judgement. Otherwise, preserve the PR's intent (the HEAD side) unless `origin/{{baseBranch}}` made a change that should be preserved (e.g. security fix, renamed API), and use judgement.
19
19
  3. After resolving, run the quality commands with Bash and fix any issues YOUR resolution introduced.
20
20
  4. Final message format (or `FAILED: <reason>` on failure):
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
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",