@kody-ade/kody-engine 0.3.9 → 0.3.11
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 +157 -28
- package/dist/executables/release/profile.json +11 -0
- package/dist/executables/resolve/apply-prefer.sh +54 -0
- package/dist/executables/resolve/profile.json +12 -0
- package/dist/executables/resolve/prompt.md +2 -2
- package/dist/executables/types.ts +14 -1
- package/package.json +1 -1
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.
|
|
6
|
+
version: "0.3.11",
|
|
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",
|
|
@@ -795,6 +795,7 @@ function coerceBare(spec, value) {
|
|
|
795
795
|
}
|
|
796
796
|
|
|
797
797
|
// src/executor.ts
|
|
798
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
798
799
|
import * as fs21 from "fs";
|
|
799
800
|
import * as path18 from "path";
|
|
800
801
|
|
|
@@ -977,7 +978,7 @@ function loadProfile(profilePath) {
|
|
|
977
978
|
function validateScriptReferences(profile, registeredScripts) {
|
|
978
979
|
const missing = [];
|
|
979
980
|
for (const e of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
980
|
-
if (!registeredScripts.has(e.script)) missing.push(e.script);
|
|
981
|
+
if (e.script && !registeredScripts.has(e.script)) missing.push(e.script);
|
|
981
982
|
}
|
|
982
983
|
return missing;
|
|
983
984
|
}
|
|
@@ -1137,11 +1138,20 @@ function parseScriptList(p, key, raw) {
|
|
|
1137
1138
|
const out = [];
|
|
1138
1139
|
for (const [i, item] of raw.entries()) {
|
|
1139
1140
|
if (!item || typeof item !== "object") {
|
|
1140
|
-
throw new ProfileError(p, `scripts.${key}[${i}] must be an object like { script, runWhen? }`);
|
|
1141
|
+
throw new ProfileError(p, `scripts.${key}[${i}] must be an object like { script, runWhen? } or { shell, runWhen? }`);
|
|
1141
1142
|
}
|
|
1142
1143
|
const r = item;
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1144
|
+
const hasScript = typeof r.script === "string" && r.script.length > 0;
|
|
1145
|
+
const hasShell = typeof r.shell === "string" && r.shell.length > 0;
|
|
1146
|
+
if (hasScript && hasShell) {
|
|
1147
|
+
throw new ProfileError(p, `scripts.${key}[${i}] cannot set both "script" and "shell" \u2014 pick one`);
|
|
1148
|
+
}
|
|
1149
|
+
if (!hasScript && !hasShell) {
|
|
1150
|
+
throw new ProfileError(p, `scripts.${key}[${i}] must set "script" (registered TS function) or "shell" (filename in executable dir)`);
|
|
1151
|
+
}
|
|
1152
|
+
const entry = {};
|
|
1153
|
+
if (hasScript) entry.script = r.script;
|
|
1154
|
+
if (hasShell) entry.shell = r.shell;
|
|
1145
1155
|
if (r.runWhen && typeof r.runWhen === "object") {
|
|
1146
1156
|
entry.runWhen = r.runWhen;
|
|
1147
1157
|
}
|
|
@@ -4327,6 +4337,24 @@ function lastReleaseTag(cwd) {
|
|
|
4327
4337
|
return null;
|
|
4328
4338
|
}
|
|
4329
4339
|
}
|
|
4340
|
+
function remoteBranchExists(branch, cwd) {
|
|
4341
|
+
try {
|
|
4342
|
+
const out = git3(["ls-remote", "--heads", "origin", branch], cwd, 3e4);
|
|
4343
|
+
return out.length > 0;
|
|
4344
|
+
} catch {
|
|
4345
|
+
return false;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
function findOpenPrForBranch(branch, cwd) {
|
|
4349
|
+
try {
|
|
4350
|
+
const out = gh2(["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--limit", "1"], { cwd });
|
|
4351
|
+
const parsed = JSON.parse(out || "[]");
|
|
4352
|
+
const first = parsed[0];
|
|
4353
|
+
return first?.url ?? null;
|
|
4354
|
+
} catch {
|
|
4355
|
+
return null;
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4330
4358
|
function runShell(cmd, cwd, timeoutMs) {
|
|
4331
4359
|
const r = spawnSync(cmd, {
|
|
4332
4360
|
cwd,
|
|
@@ -4341,6 +4369,7 @@ var releaseFlow = async (ctx) => {
|
|
|
4341
4369
|
const mode = ctx.args.mode ?? "prepare";
|
|
4342
4370
|
const bump = ctx.args.bump ?? "patch";
|
|
4343
4371
|
const dryRun = ctx.args["dry-run"] === true || ctx.args.dryRun === true;
|
|
4372
|
+
const prefer = ctx.args.prefer ?? void 0;
|
|
4344
4373
|
const issueNumber = typeof ctx.args.issue === "number" ? ctx.args.issue : void 0;
|
|
4345
4374
|
const cwd = ctx.cwd;
|
|
4346
4375
|
const releaseCfg = ctx.config.release ?? {};
|
|
@@ -4348,7 +4377,7 @@ var releaseFlow = async (ctx) => {
|
|
|
4348
4377
|
const timeoutMs = releaseCfg.timeoutMs ?? 6e5;
|
|
4349
4378
|
ctx.skipAgent = true;
|
|
4350
4379
|
if (mode === "prepare") {
|
|
4351
|
-
await runPrepare({ cwd, bump, dryRun, versionFiles, ctx });
|
|
4380
|
+
await runPrepare({ cwd, bump, dryRun, prefer, versionFiles, ctx });
|
|
4352
4381
|
} else if (mode === "finalize") {
|
|
4353
4382
|
await runFinalize({ cwd, dryRun, timeoutMs, releaseCfg, ctx });
|
|
4354
4383
|
} else {
|
|
@@ -4378,7 +4407,7 @@ function buildIssueNotice(mode, dryRun, ctx) {
|
|
|
4378
4407
|
return `\u2705 kody ${label} complete`;
|
|
4379
4408
|
}
|
|
4380
4409
|
async function runPrepare(args) {
|
|
4381
|
-
const { cwd, bump, dryRun, versionFiles, ctx } = args;
|
|
4410
|
+
const { cwd, bump, dryRun, prefer, versionFiles, ctx } = args;
|
|
4382
4411
|
const pkgPath = path17.join(cwd, "package.json");
|
|
4383
4412
|
if (!fs19.existsSync(pkgPath)) {
|
|
4384
4413
|
ctx.output.exitCode = 99;
|
|
@@ -4398,11 +4427,35 @@ async function runPrepare(args) {
|
|
|
4398
4427
|
`);
|
|
4399
4428
|
if (dryRun) {
|
|
4400
4429
|
ctx.output.exitCode = 0;
|
|
4401
|
-
ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}`;
|
|
4430
|
+
ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}${prefer ? ` (--prefer ${prefer})` : ""}`;
|
|
4402
4431
|
process.stdout.write(`RELEASE_PLAN=bump=${newVersion} tag=${tag}
|
|
4403
4432
|
`);
|
|
4404
4433
|
return;
|
|
4405
4434
|
}
|
|
4435
|
+
const releaseBranch = `release/${tag}`;
|
|
4436
|
+
const collides = remoteBranchExists(releaseBranch, cwd);
|
|
4437
|
+
if (collides) {
|
|
4438
|
+
if (prefer === "theirs") {
|
|
4439
|
+
const existingPr = findOpenPrForBranch(releaseBranch, cwd);
|
|
4440
|
+
if (existingPr) {
|
|
4441
|
+
process.stdout.write(` reusing existing PR (--prefer theirs): ${existingPr}
|
|
4442
|
+
`);
|
|
4443
|
+
ctx.output.prUrl = existingPr;
|
|
4444
|
+
ctx.output.exitCode = 0;
|
|
4445
|
+
return;
|
|
4446
|
+
}
|
|
4447
|
+
ctx.output.exitCode = 4;
|
|
4448
|
+
ctx.output.reason = `release prepare --prefer theirs: ${releaseBranch} exists on remote but has no open PR \u2014 nothing to reuse`;
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
if (prefer !== "ours") {
|
|
4452
|
+
ctx.output.exitCode = 4;
|
|
4453
|
+
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.`;
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
process.stdout.write(` branch ${releaseBranch} exists on remote \u2014 will force-push (--prefer ours)
|
|
4457
|
+
`);
|
|
4458
|
+
}
|
|
4406
4459
|
const touched = [];
|
|
4407
4460
|
for (const f of versionFiles) {
|
|
4408
4461
|
if (updateVersionInFile(f, newVersion, cwd)) touched.push(f);
|
|
@@ -4418,12 +4471,12 @@ async function runPrepare(args) {
|
|
|
4418
4471
|
prependChangelog(cwd, entry);
|
|
4419
4472
|
process.stdout.write(` wrote CHANGELOG.md
|
|
4420
4473
|
`);
|
|
4421
|
-
const releaseBranch = `release/${tag}`;
|
|
4422
4474
|
try {
|
|
4423
4475
|
git3(["checkout", "-b", releaseBranch], cwd);
|
|
4424
4476
|
for (const f of [...touched, "CHANGELOG.md"]) git3(["add", "--", f], cwd);
|
|
4425
4477
|
git3(["commit", "--no-gpg-sign", "-m", `chore: release ${tag}`], cwd);
|
|
4426
|
-
|
|
4478
|
+
const pushArgs = collides && prefer === "ours" ? ["push", "-u", "--force-with-lease", "origin", releaseBranch] : ["push", "-u", "origin", releaseBranch];
|
|
4479
|
+
git3(pushArgs, cwd, 12e4);
|
|
4427
4480
|
} catch (err) {
|
|
4428
4481
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4429
4482
|
ctx.output.exitCode = 4;
|
|
@@ -4442,16 +4495,23 @@ ${rawEntry}
|
|
|
4442
4495
|
|
|
4443
4496
|
Merge this and then run \`kody release --mode finalize\`.`;
|
|
4444
4497
|
let prUrl = "";
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4498
|
+
const preexistingPr = collides && prefer === "ours" ? findOpenPrForBranch(releaseBranch, cwd) : null;
|
|
4499
|
+
if (preexistingPr) {
|
|
4500
|
+
process.stdout.write(` PR already open for ${releaseBranch}: ${preexistingPr}
|
|
4501
|
+
`);
|
|
4502
|
+
prUrl = preexistingPr;
|
|
4503
|
+
} else {
|
|
4504
|
+
try {
|
|
4505
|
+
prUrl = gh2(["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"], {
|
|
4506
|
+
input: body,
|
|
4507
|
+
cwd
|
|
4508
|
+
}).trim();
|
|
4509
|
+
} catch (err) {
|
|
4510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4511
|
+
ctx.output.exitCode = 4;
|
|
4512
|
+
ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
|
|
4513
|
+
return;
|
|
4514
|
+
}
|
|
4455
4515
|
}
|
|
4456
4516
|
ctx.output.prUrl = prUrl;
|
|
4457
4517
|
ctx.output.exitCode = 0;
|
|
@@ -4681,6 +4741,7 @@ var resolveFlow = async (ctx) => {
|
|
|
4681
4741
|
}
|
|
4682
4742
|
ctx.data.conflictedFiles = conflictedFiles;
|
|
4683
4743
|
ctx.data.conflictMarkersPreview = getConflictMarkersPreview(conflictedFiles, ctx.cwd);
|
|
4744
|
+
ctx.data.preferBlock = buildPreferBlock(ctx.args.prefer, baseBranch);
|
|
4684
4745
|
const runUrl = getRunUrl();
|
|
4685
4746
|
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
4686
4747
|
tryPostPr3(
|
|
@@ -4689,6 +4750,23 @@ var resolveFlow = async (ctx) => {
|
|
|
4689
4750
|
ctx.cwd
|
|
4690
4751
|
);
|
|
4691
4752
|
};
|
|
4753
|
+
function buildPreferBlock(prefer, baseBranch) {
|
|
4754
|
+
if (prefer !== "ours" && prefer !== "theirs") return "";
|
|
4755
|
+
const keepSide = prefer === "ours" ? "HEAD (this PR branch)" : `origin/${baseBranch} (base branch)`;
|
|
4756
|
+
const keepMarkers = prefer === "ours" ? "content between `<<<<<<< HEAD` and `=======`" : `content between \`=======\` and \`>>>>>>> origin/${baseBranch}\``;
|
|
4757
|
+
const dropSide = prefer === "ours" ? `origin/${baseBranch}` : "HEAD";
|
|
4758
|
+
return [
|
|
4759
|
+
"# Conflict resolution directive (AUTHORITATIVE \u2014 overrides defaults below)",
|
|
4760
|
+
"",
|
|
4761
|
+
`The user requested \`--prefer ${prefer}\`. For **every** conflict in **every** file:`,
|
|
4762
|
+
"",
|
|
4763
|
+
`- Keep the **${prefer}** side: ${keepSide} \u2014 ${keepMarkers}.`,
|
|
4764
|
+
`- Discard the **${prefer === "ours" ? "theirs" : "ours"}** side (from ${dropSide}) entirely.`,
|
|
4765
|
+
"- Remove all `<<<<<<<`, `=======`, `>>>>>>>` markers.",
|
|
4766
|
+
"- Do NOT attempt to merge the two sides or apply judgement.",
|
|
4767
|
+
""
|
|
4768
|
+
].join("\n");
|
|
4769
|
+
}
|
|
4692
4770
|
function getConflictedFiles(cwd) {
|
|
4693
4771
|
try {
|
|
4694
4772
|
const out = execFileSync17("git", ["diff", "--name-only", "--diff-filter=U"], {
|
|
@@ -5373,9 +5451,13 @@ async function runExecutable(profileName, input) {
|
|
|
5373
5451
|
try {
|
|
5374
5452
|
for (const entry of profile.scripts.preflight) {
|
|
5375
5453
|
if (!shouldRun(entry, ctx)) continue;
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5454
|
+
if (entry.shell) {
|
|
5455
|
+
runShellEntry(entry, ctx, profile);
|
|
5456
|
+
} else {
|
|
5457
|
+
const fn = preflightScripts[entry.script];
|
|
5458
|
+
if (!fn) return finish({ exitCode: 99, reason: `preflight script not registered: ${entry.script}` });
|
|
5459
|
+
await fn(ctx, profile, entry.with);
|
|
5460
|
+
}
|
|
5379
5461
|
if (ctx.skipAgent && ctx.output.exitCode !== void 0 && ctx.output.exitCode !== 0) {
|
|
5380
5462
|
return finish(ctx.output);
|
|
5381
5463
|
}
|
|
@@ -5390,15 +5472,20 @@ async function runExecutable(profileName, input) {
|
|
|
5390
5472
|
}
|
|
5391
5473
|
for (const entry of profile.scripts.postflight) {
|
|
5392
5474
|
if (!shouldRun(entry, ctx)) continue;
|
|
5393
|
-
const
|
|
5394
|
-
if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
|
|
5475
|
+
const label = entry.script ?? entry.shell ?? "<unknown>";
|
|
5395
5476
|
try {
|
|
5396
|
-
|
|
5477
|
+
if (entry.shell) {
|
|
5478
|
+
runShellEntry(entry, ctx, profile);
|
|
5479
|
+
} else {
|
|
5480
|
+
const fn = postflightScripts[entry.script];
|
|
5481
|
+
if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
|
|
5482
|
+
await fn(ctx, profile, agentResult, entry.with);
|
|
5483
|
+
}
|
|
5397
5484
|
} catch (err) {
|
|
5398
5485
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5399
|
-
process.stderr.write(`[kody] postflight
|
|
5486
|
+
process.stderr.write(`[kody] postflight "${label}" crashed: ${msg}
|
|
5400
5487
|
`);
|
|
5401
|
-
if (!ctx.output.reason) ctx.output.reason = `postflight ${
|
|
5488
|
+
if (!ctx.output.reason) ctx.output.reason = `postflight ${label} crashed: ${msg}`;
|
|
5402
5489
|
if (ctx.output.exitCode === 0) ctx.output.exitCode = 99;
|
|
5403
5490
|
}
|
|
5404
5491
|
}
|
|
@@ -5515,6 +5602,48 @@ function finish(out) {
|
|
|
5515
5602
|
`);
|
|
5516
5603
|
return out;
|
|
5517
5604
|
}
|
|
5605
|
+
var SHELL_TIMEOUT_MS = 3e5;
|
|
5606
|
+
function runShellEntry(entry, ctx, profile) {
|
|
5607
|
+
const shellName = entry.shell;
|
|
5608
|
+
const shellPath = path18.join(profile.dir, shellName);
|
|
5609
|
+
if (!fs21.existsSync(shellPath)) {
|
|
5610
|
+
ctx.skipAgent = true;
|
|
5611
|
+
ctx.output.exitCode = 99;
|
|
5612
|
+
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
5613
|
+
return;
|
|
5614
|
+
}
|
|
5615
|
+
const positional = entry.with ? Object.values(entry.with).map((v) => String(v)) : [];
|
|
5616
|
+
const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
5617
|
+
for (const [k, v] of Object.entries(ctx.args)) {
|
|
5618
|
+
if (v === void 0 || v === null) continue;
|
|
5619
|
+
env[`KODY_ARG_${k.toUpperCase().replace(/-/g, "_")}`] = String(v);
|
|
5620
|
+
}
|
|
5621
|
+
const r = spawnSync2("bash", [shellPath, ...positional], {
|
|
5622
|
+
cwd: ctx.cwd,
|
|
5623
|
+
encoding: "utf-8",
|
|
5624
|
+
env,
|
|
5625
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
5626
|
+
timeout: SHELL_TIMEOUT_MS
|
|
5627
|
+
});
|
|
5628
|
+
const stdout = r.stdout ?? "";
|
|
5629
|
+
const stderr = r.stderr ?? "";
|
|
5630
|
+
if (stdout) process.stdout.write(stdout);
|
|
5631
|
+
if (stderr) process.stderr.write(stderr);
|
|
5632
|
+
if (/^KODY_SKIP_AGENT=true\s*$/m.test(stdout)) {
|
|
5633
|
+
ctx.skipAgent = true;
|
|
5634
|
+
}
|
|
5635
|
+
const exit = r.status ?? -1;
|
|
5636
|
+
if (exit !== 0) {
|
|
5637
|
+
ctx.skipAgent = true;
|
|
5638
|
+
if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
|
|
5639
|
+
ctx.output.exitCode = exit;
|
|
5640
|
+
}
|
|
5641
|
+
if (!ctx.output.reason) {
|
|
5642
|
+
const tail = (stderr || stdout).slice(-800);
|
|
5643
|
+
ctx.output.reason = `shell '${shellName}' exited ${exit}${tail ? `: ${tail}` : ""}`;
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5518
5647
|
|
|
5519
5648
|
// src/kody-cli.ts
|
|
5520
5649
|
var CI_HELP = `kody ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
|
|
@@ -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": {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Deterministic conflict resolution for `kody resolve --prefer ours|theirs`.
|
|
4
|
+
#
|
|
5
|
+
# Runs as a preflight shell entry (see src/executables/resolve/profile.json).
|
|
6
|
+
# Gated by runWhen on args.prefer, so it only fires when the user requested
|
|
7
|
+
# a side. Reads the side from $KODY_ARG_PREFER (the executor exposes every
|
|
8
|
+
# ctx.args.<key> as an env var).
|
|
9
|
+
#
|
|
10
|
+
# Preconditions set by the prior `resolveFlow` preflight:
|
|
11
|
+
# - cwd is on the PR branch.
|
|
12
|
+
# - `git merge origin/<base> --no-edit --no-ff` already ran and conflicted.
|
|
13
|
+
# - Working tree has unmerged paths.
|
|
14
|
+
#
|
|
15
|
+
# Behavior: for each unmerged file, git checkout --ours (or --theirs), add,
|
|
16
|
+
# commit the merge, push the branch. Prints `KODY_SKIP_AGENT=true` on
|
|
17
|
+
# success so the executor bypasses the agent entirely.
|
|
18
|
+
#
|
|
19
|
+
# Exits:
|
|
20
|
+
# 0 — resolved + pushed (or nothing to resolve)
|
|
21
|
+
# 64 — invalid side value
|
|
22
|
+
# 1+ — git operation failed (executor will surface stderr)
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
side="${KODY_ARG_PREFER:-}"
|
|
27
|
+
if [[ "$side" != "ours" && "$side" != "theirs" ]]; then
|
|
28
|
+
echo "apply-prefer: expected KODY_ARG_PREFER=ours|theirs, got '$side'" >&2
|
|
29
|
+
exit 64
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
unmerged=$(git diff --name-only --diff-filter=U)
|
|
33
|
+
if [[ -z "$unmerged" ]]; then
|
|
34
|
+
echo "apply-prefer: no unmerged paths — nothing to resolve"
|
|
35
|
+
echo "KODY_SKIP_AGENT=true"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
count=0
|
|
40
|
+
while IFS= read -r f; do
|
|
41
|
+
[[ -z "$f" ]] && continue
|
|
42
|
+
git checkout "--$side" -- "$f"
|
|
43
|
+
git add -- "$f"
|
|
44
|
+
count=$((count + 1))
|
|
45
|
+
done <<< "$unmerged"
|
|
46
|
+
echo "apply-prefer: resolved $count file(s) via --$side"
|
|
47
|
+
|
|
48
|
+
# Complete the merge. git merge left MERGE_MSG in place; --no-edit uses it.
|
|
49
|
+
HUSKY=0 SKIP_HOOKS=1 git -c commit.gpgsign=false commit --no-edit
|
|
50
|
+
|
|
51
|
+
branch=$(git rev-parse --abbrev-ref HEAD)
|
|
52
|
+
git push origin "$branch"
|
|
53
|
+
echo "apply-prefer: pushed $branch"
|
|
54
|
+
echo "KODY_SKIP_AGENT=true"
|
|
@@ -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": {
|
|
@@ -45,6 +53,10 @@
|
|
|
45
53
|
{
|
|
46
54
|
"script": "resolveFlow"
|
|
47
55
|
},
|
|
56
|
+
{
|
|
57
|
+
"shell": "apply-prefer.sh",
|
|
58
|
+
"runWhen": { "args.prefer": ["ours", "theirs"] }
|
|
59
|
+
},
|
|
48
60
|
{
|
|
49
61
|
"script": "loadTaskState"
|
|
50
62
|
},
|
|
@@ -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.
|
|
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
|
|
|
@@ -159,7 +159,20 @@ export interface CliToolSpec {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export interface ScriptEntry {
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Name of a registered TS function in src/scripts/index.ts. Mutually
|
|
164
|
+
* exclusive with `shell` — exactly one must be set.
|
|
165
|
+
*/
|
|
166
|
+
script?: string
|
|
167
|
+
/**
|
|
168
|
+
* Filename of a shell script colocated with the executable
|
|
169
|
+
* (e.g. "apply-prefer.sh"). Resolved relative to the profile's
|
|
170
|
+
* directory. Invoked via `bash <path> <with-args>` with ctx.args
|
|
171
|
+
* exposed as env vars (KODY_ARG_<UPPER_NAME>=<value>). A stdout
|
|
172
|
+
* line `KODY_SKIP_AGENT=true` signals the executor to bypass the
|
|
173
|
+
* agent. Non-zero exit is treated as a preflight failure.
|
|
174
|
+
*/
|
|
175
|
+
shell?: string
|
|
163
176
|
/**
|
|
164
177
|
* Optional conditional. Keys are dotted paths into the context (e.g.
|
|
165
178
|
* "args.mode"). Values are a single primitive or an array of primitives.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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",
|