@kody-ade/kody-engine 0.3.12 → 0.3.14

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.12",
6
+ version: "0.3.14",
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",
@@ -1163,11 +1163,187 @@ function parseScriptList(p, key, raw) {
1163
1163
  return out;
1164
1164
  }
1165
1165
 
1166
+ // src/commit.ts
1167
+ import { execFileSync as execFileSync2 } from "child_process";
1168
+ import * as fs9 from "fs";
1169
+ import * as path8 from "path";
1170
+ var FORBIDDEN_PATH_PREFIXES = [
1171
+ ".kody/",
1172
+ ".kody-engine/",
1173
+ ".kody/",
1174
+ ".kody-lean/",
1175
+ // back-compat: stale runtime dir from kody-lean v0.5.x
1176
+ "node_modules/",
1177
+ "dist/",
1178
+ "build/"
1179
+ ];
1180
+ var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
1181
+ var FORBIDDEN_PATH_SUFFIXES = [".log"];
1182
+ var CONVENTIONAL_PREFIXES = [
1183
+ "feat:",
1184
+ "fix:",
1185
+ "chore:",
1186
+ "docs:",
1187
+ "refactor:",
1188
+ "test:",
1189
+ "perf:",
1190
+ "ci:",
1191
+ "style:",
1192
+ "build:",
1193
+ "revert:"
1194
+ ];
1195
+ function git(args, cwd) {
1196
+ try {
1197
+ return execFileSync2("git", args, {
1198
+ encoding: "utf-8",
1199
+ timeout: 12e4,
1200
+ cwd,
1201
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1202
+ stdio: ["pipe", "pipe", "pipe"]
1203
+ }).trim();
1204
+ } catch (err) {
1205
+ const e = err;
1206
+ const stderr = e.stderr?.toString().trim() ?? "";
1207
+ const stdout = e.stdout?.toString().trim() ?? "";
1208
+ const status = e.status ?? "?";
1209
+ const detail = stderr || stdout || e.message || "(no output)";
1210
+ throw new Error(`git ${args.join(" ")} (exit ${status}):
1211
+ ${detail}`);
1212
+ }
1213
+ }
1214
+ function tryGit(args, cwd) {
1215
+ try {
1216
+ git(args, cwd);
1217
+ return true;
1218
+ } catch {
1219
+ return false;
1220
+ }
1221
+ }
1222
+ function abortUnfinishedGitOps(cwd) {
1223
+ const aborted = [];
1224
+ const gitDir = path8.join(cwd ?? process.cwd(), ".git");
1225
+ if (!fs9.existsSync(gitDir)) return aborted;
1226
+ if (fs9.existsSync(path8.join(gitDir, "MERGE_HEAD"))) {
1227
+ if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1228
+ }
1229
+ if (fs9.existsSync(path8.join(gitDir, "CHERRY_PICK_HEAD"))) {
1230
+ if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1231
+ }
1232
+ if (fs9.existsSync(path8.join(gitDir, "REVERT_HEAD"))) {
1233
+ if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1234
+ }
1235
+ if (fs9.existsSync(path8.join(gitDir, "rebase-merge")) || fs9.existsSync(path8.join(gitDir, "rebase-apply"))) {
1236
+ if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1237
+ }
1238
+ try {
1239
+ const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
1240
+ if (unmerged) {
1241
+ tryGit(["reset", "--mixed", "HEAD"], cwd);
1242
+ aborted.push("unmerged-paths-reset");
1243
+ }
1244
+ } catch {
1245
+ }
1246
+ return aborted;
1247
+ }
1248
+ function isForbiddenPath(p) {
1249
+ if (FORBIDDEN_PATH_EXACT.has(p)) return true;
1250
+ for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
1251
+ for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
1252
+ return false;
1253
+ }
1254
+ function listChangedFiles(cwd) {
1255
+ const raw = execFileSync2("git", ["status", "--porcelain=v1", "-z"], {
1256
+ encoding: "utf-8",
1257
+ cwd,
1258
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1259
+ stdio: ["pipe", "pipe", "pipe"]
1260
+ });
1261
+ if (!raw) return [];
1262
+ const entries = raw.split("\0").filter((e) => e.length > 0);
1263
+ return entries.map((e) => e.slice(3)).filter(Boolean);
1264
+ }
1265
+ function listFilesInCommit(ref = "HEAD", cwd) {
1266
+ try {
1267
+ const raw = execFileSync2("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1268
+ encoding: "utf-8",
1269
+ cwd,
1270
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1271
+ stdio: ["pipe", "pipe", "pipe"]
1272
+ });
1273
+ return raw.split("\0").map((s) => s.trim()).filter(Boolean);
1274
+ } catch {
1275
+ return [];
1276
+ }
1277
+ }
1278
+ function normalizeCommitMessage(raw) {
1279
+ const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
1280
+ if (!trimmed) return "chore: kody update";
1281
+ const firstLine2 = trimmed.split("\n")[0];
1282
+ for (const prefix of CONVENTIONAL_PREFIXES) {
1283
+ if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
1284
+ }
1285
+ return `chore: ${trimmed}`;
1286
+ }
1287
+ function commitAndPush(branch, agentMessage, cwd) {
1288
+ const allChanged = listChangedFiles(cwd);
1289
+ const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1290
+ const mergeHeadExists = fs9.existsSync(path8.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1291
+ if (allowedFiles.length === 0 && !mergeHeadExists) {
1292
+ return { committed: false, pushed: false, sha: "", message: "" };
1293
+ }
1294
+ for (const f of allowedFiles) {
1295
+ try {
1296
+ git(["add", "--", f], cwd);
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ const message = normalizeCommitMessage(agentMessage);
1301
+ try {
1302
+ git(["commit", "--no-gpg-sign", "-m", message], cwd);
1303
+ } catch (err) {
1304
+ const msg = err instanceof Error ? err.message : String(err);
1305
+ if (/nothing to commit/i.test(msg)) {
1306
+ return { committed: false, pushed: false, sha: "", message };
1307
+ }
1308
+ throw err;
1309
+ }
1310
+ const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
1311
+ try {
1312
+ git(["push", "-u", "origin", branch], cwd);
1313
+ } catch {
1314
+ git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
1315
+ }
1316
+ return { committed: true, pushed: true, sha, message };
1317
+ }
1318
+ function hasCommitsAhead(branch, defaultBranch, cwd) {
1319
+ try {
1320
+ const out = git(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
1321
+ return parseInt(out, 10) > 0;
1322
+ } catch {
1323
+ try {
1324
+ const out = git(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
1325
+ return parseInt(out, 10) > 0;
1326
+ } catch {
1327
+ return false;
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ // src/scripts/abortUnfinishedGitOps.ts
1333
+ var abortUnfinishedGitOps2 = async (ctx) => {
1334
+ if (ctx.data.agentDone === false) return;
1335
+ const aborted = abortUnfinishedGitOps(ctx.cwd);
1336
+ if (aborted.length > 0) {
1337
+ process.stderr.write(`[kody] cleaned up unfinished git ops: ${aborted.join(", ")}
1338
+ `);
1339
+ }
1340
+ };
1341
+
1166
1342
  // src/scripts/advanceFlow.ts
1167
- import { execFileSync as execFileSync3 } from "child_process";
1343
+ import { execFileSync as execFileSync4 } from "child_process";
1168
1344
 
1169
1345
  // src/state.ts
1170
- import { execFileSync as execFileSync2 } from "child_process";
1346
+ import { execFileSync as execFileSync3 } from "child_process";
1171
1347
  var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
1172
1348
  var STATE_END = "<!-- kody:state:v1:end -->";
1173
1349
  var HISTORY_MAX_ENTRIES = 20;
@@ -1193,7 +1369,7 @@ function ghToken() {
1193
1369
  function gh(args, input, cwd) {
1194
1370
  const token = ghToken();
1195
1371
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1196
- return execFileSync2("gh", args, {
1372
+ return execFileSync3("gh", args, {
1197
1373
  encoding: "utf-8",
1198
1374
  timeout: API_TIMEOUT_MS,
1199
1375
  cwd,
@@ -1396,7 +1572,7 @@ var advanceFlow = async (ctx, profile) => {
1396
1572
  }
1397
1573
  const body = `@kody ${flow.name}`;
1398
1574
  try {
1399
- execFileSync3("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
1575
+ execFileSync4("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
1400
1576
  timeout: API_TIMEOUT_MS2,
1401
1577
  cwd: ctx.cwd,
1402
1578
  stdio: ["ignore", "pipe", "pipe"]
@@ -1410,21 +1586,21 @@ var advanceFlow = async (ctx, profile) => {
1410
1586
  };
1411
1587
 
1412
1588
  // src/scripts/buildSyntheticPlugin.ts
1413
- import * as fs9 from "fs";
1589
+ import * as fs10 from "fs";
1414
1590
  import * as os2 from "os";
1415
- import * as path8 from "path";
1591
+ import * as path9 from "path";
1416
1592
  function getPluginsCatalogRoot() {
1417
- const here = path8.dirname(new URL(import.meta.url).pathname);
1593
+ const here = path9.dirname(new URL(import.meta.url).pathname);
1418
1594
  const candidates = [
1419
- path8.join(here, "..", "plugins"),
1595
+ path9.join(here, "..", "plugins"),
1420
1596
  // dev: src/scripts → src/plugins
1421
- path8.join(here, "..", "..", "plugins"),
1597
+ path9.join(here, "..", "..", "plugins"),
1422
1598
  // built: dist/scripts → dist/plugins
1423
- path8.join(here, "..", "..", "src", "plugins")
1599
+ path9.join(here, "..", "..", "src", "plugins")
1424
1600
  // fallback
1425
1601
  ];
1426
1602
  for (const c of candidates) {
1427
- if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1603
+ if (fs10.existsSync(c) && fs10.statSync(c).isDirectory()) return c;
1428
1604
  }
1429
1605
  return candidates[0];
1430
1606
  }
@@ -1434,50 +1610,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1434
1610
  if (!needsSynthetic) return;
1435
1611
  const catalog = getPluginsCatalogRoot();
1436
1612
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1437
- const root = path8.join(os2.tmpdir(), `kody-synth-${runId}`);
1438
- fs9.mkdirSync(path8.join(root, ".claude-plugin"), { recursive: true });
1613
+ const root = path9.join(os2.tmpdir(), `kody-synth-${runId}`);
1614
+ fs10.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
1439
1615
  if (cc.skills.length > 0) {
1440
- const dst = path8.join(root, "skills");
1441
- fs9.mkdirSync(dst, { recursive: true });
1616
+ const dst = path9.join(root, "skills");
1617
+ fs10.mkdirSync(dst, { recursive: true });
1442
1618
  for (const name of cc.skills) {
1443
- const src = path8.join(catalog, "skills", name);
1444
- if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1445
- copyDir(src, path8.join(dst, name));
1619
+ const src = path9.join(catalog, "skills", name);
1620
+ if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1621
+ copyDir(src, path9.join(dst, name));
1446
1622
  }
1447
1623
  }
1448
1624
  if (cc.commands.length > 0) {
1449
- const dst = path8.join(root, "commands");
1450
- fs9.mkdirSync(dst, { recursive: true });
1625
+ const dst = path9.join(root, "commands");
1626
+ fs10.mkdirSync(dst, { recursive: true });
1451
1627
  for (const name of cc.commands) {
1452
- const src = path8.join(catalog, "commands", `${name}.md`);
1453
- if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1454
- fs9.copyFileSync(src, path8.join(dst, `${name}.md`));
1628
+ const src = path9.join(catalog, "commands", `${name}.md`);
1629
+ if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1630
+ fs10.copyFileSync(src, path9.join(dst, `${name}.md`));
1455
1631
  }
1456
1632
  }
1457
1633
  if (cc.subagents.length > 0) {
1458
- const dst = path8.join(root, "agents");
1459
- fs9.mkdirSync(dst, { recursive: true });
1634
+ const dst = path9.join(root, "agents");
1635
+ fs10.mkdirSync(dst, { recursive: true });
1460
1636
  for (const name of cc.subagents) {
1461
- const src = path8.join(catalog, "agents", `${name}.md`);
1462
- if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1463
- fs9.copyFileSync(src, path8.join(dst, `${name}.md`));
1637
+ const src = path9.join(catalog, "agents", `${name}.md`);
1638
+ if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1639
+ fs10.copyFileSync(src, path9.join(dst, `${name}.md`));
1464
1640
  }
1465
1641
  }
1466
1642
  if (cc.hooks.length > 0) {
1467
- const dst = path8.join(root, "hooks");
1468
- fs9.mkdirSync(dst, { recursive: true });
1643
+ const dst = path9.join(root, "hooks");
1644
+ fs10.mkdirSync(dst, { recursive: true });
1469
1645
  const merged = { hooks: {} };
1470
1646
  for (const name of cc.hooks) {
1471
- const src = path8.join(catalog, "hooks", `${name}.json`);
1472
- if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1473
- const parsed = JSON.parse(fs9.readFileSync(src, "utf-8"));
1647
+ const src = path9.join(catalog, "hooks", `${name}.json`);
1648
+ if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1649
+ const parsed = JSON.parse(fs10.readFileSync(src, "utf-8"));
1474
1650
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1475
1651
  if (!Array.isArray(entries)) continue;
1476
1652
  if (!merged.hooks[event]) merged.hooks[event] = [];
1477
1653
  merged.hooks[event].push(...entries);
1478
1654
  }
1479
1655
  }
1480
- fs9.writeFileSync(path8.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1656
+ fs10.writeFileSync(path9.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1481
1657
  `);
1482
1658
  }
1483
1659
  const manifest = {
@@ -1488,22 +1664,22 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1488
1664
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1489
1665
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1490
1666
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1491
- fs9.writeFileSync(path8.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1667
+ fs10.writeFileSync(path9.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1492
1668
  `);
1493
1669
  ctx.data.syntheticPluginPath = root;
1494
1670
  };
1495
1671
  function copyDir(src, dst) {
1496
- fs9.mkdirSync(dst, { recursive: true });
1497
- for (const ent of fs9.readdirSync(src, { withFileTypes: true })) {
1498
- const s = path8.join(src, ent.name);
1499
- const d = path8.join(dst, ent.name);
1672
+ fs10.mkdirSync(dst, { recursive: true });
1673
+ for (const ent of fs10.readdirSync(src, { withFileTypes: true })) {
1674
+ const s = path9.join(src, ent.name);
1675
+ const d = path9.join(dst, ent.name);
1500
1676
  if (ent.isDirectory()) copyDir(s, d);
1501
- else if (ent.isFile()) fs9.copyFileSync(s, d);
1677
+ else if (ent.isFile()) fs10.copyFileSync(s, d);
1502
1678
  }
1503
1679
  }
1504
1680
 
1505
1681
  // src/coverage.ts
1506
- import { execFileSync as execFileSync4 } from "child_process";
1682
+ import { execFileSync as execFileSync5 } from "child_process";
1507
1683
  function patternToRegex(pattern) {
1508
1684
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
1509
1685
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -1521,7 +1697,7 @@ function renderSiblingPath(file, requireSibling) {
1521
1697
  }
1522
1698
  function safeGit(args, cwd) {
1523
1699
  try {
1524
- return execFileSync4("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
1700
+ return execFileSync5("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
1525
1701
  } catch {
1526
1702
  return "";
1527
1703
  }
@@ -1564,18 +1740,18 @@ function formatMissesForFeedback(misses) {
1564
1740
  }
1565
1741
 
1566
1742
  // src/prompt.ts
1567
- import * as fs10 from "fs";
1568
- import * as path9 from "path";
1743
+ import * as fs11 from "fs";
1744
+ import * as path10 from "path";
1569
1745
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1570
1746
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1571
1747
  function loadProjectConventions(projectDir) {
1572
1748
  const out = [];
1573
1749
  for (const rel of CONVENTION_FILES) {
1574
- const abs = path9.join(projectDir, rel);
1575
- if (!fs10.existsSync(abs)) continue;
1750
+ const abs = path10.join(projectDir, rel);
1751
+ if (!fs11.existsSync(abs)) continue;
1576
1752
  let content;
1577
1753
  try {
1578
- content = fs10.readFileSync(abs, "utf-8");
1754
+ content = fs11.readFileSync(abs, "utf-8");
1579
1755
  } catch {
1580
1756
  continue;
1581
1757
  }
@@ -1735,176 +1911,8 @@ function defaultLabelMap() {
1735
1911
  }
1736
1912
 
1737
1913
  // src/scripts/commitAndPush.ts
1738
- import { execFileSync as execFileSync6 } from "child_process";
1739
-
1740
- // src/commit.ts
1741
- import { execFileSync as execFileSync5 } from "child_process";
1742
- import * as fs11 from "fs";
1743
- import * as path10 from "path";
1744
- var FORBIDDEN_PATH_PREFIXES = [
1745
- ".kody/",
1746
- ".kody-engine/",
1747
- ".kody/",
1748
- ".kody-lean/",
1749
- // back-compat: stale runtime dir from kody-lean v0.5.x
1750
- "node_modules/",
1751
- "dist/",
1752
- "build/"
1753
- ];
1754
- var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
1755
- var FORBIDDEN_PATH_SUFFIXES = [".log"];
1756
- var CONVENTIONAL_PREFIXES = [
1757
- "feat:",
1758
- "fix:",
1759
- "chore:",
1760
- "docs:",
1761
- "refactor:",
1762
- "test:",
1763
- "perf:",
1764
- "ci:",
1765
- "style:",
1766
- "build:",
1767
- "revert:"
1768
- ];
1769
- function git(args, cwd) {
1770
- try {
1771
- return execFileSync5("git", args, {
1772
- encoding: "utf-8",
1773
- timeout: 12e4,
1774
- cwd,
1775
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1776
- stdio: ["pipe", "pipe", "pipe"]
1777
- }).trim();
1778
- } catch (err) {
1779
- const e = err;
1780
- const stderr = e.stderr?.toString().trim() ?? "";
1781
- const stdout = e.stdout?.toString().trim() ?? "";
1782
- const status = e.status ?? "?";
1783
- const detail = stderr || stdout || e.message || "(no output)";
1784
- throw new Error(`git ${args.join(" ")} (exit ${status}):
1785
- ${detail}`);
1786
- }
1787
- }
1788
- function tryGit(args, cwd) {
1789
- try {
1790
- git(args, cwd);
1791
- return true;
1792
- } catch {
1793
- return false;
1794
- }
1795
- }
1796
- function abortUnfinishedGitOps(cwd) {
1797
- const aborted = [];
1798
- const gitDir = path10.join(cwd ?? process.cwd(), ".git");
1799
- if (!fs11.existsSync(gitDir)) return aborted;
1800
- if (fs11.existsSync(path10.join(gitDir, "MERGE_HEAD"))) {
1801
- if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1802
- }
1803
- if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
1804
- if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1805
- }
1806
- if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
1807
- if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1808
- }
1809
- if (fs11.existsSync(path10.join(gitDir, "rebase-merge")) || fs11.existsSync(path10.join(gitDir, "rebase-apply"))) {
1810
- if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1811
- }
1812
- try {
1813
- const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
1814
- if (unmerged) {
1815
- tryGit(["reset", "--mixed", "HEAD"], cwd);
1816
- aborted.push("unmerged-paths-reset");
1817
- }
1818
- } catch {
1819
- }
1820
- return aborted;
1821
- }
1822
- function isForbiddenPath(p) {
1823
- if (FORBIDDEN_PATH_EXACT.has(p)) return true;
1824
- for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
1825
- for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
1826
- return false;
1827
- }
1828
- function listChangedFiles(cwd) {
1829
- const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
1830
- encoding: "utf-8",
1831
- cwd,
1832
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1833
- stdio: ["pipe", "pipe", "pipe"]
1834
- });
1835
- if (!raw) return [];
1836
- const entries = raw.split("\0").filter((e) => e.length > 0);
1837
- return entries.map((e) => e.slice(3)).filter(Boolean);
1838
- }
1839
- function listFilesInCommit(ref = "HEAD", cwd) {
1840
- try {
1841
- const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1842
- encoding: "utf-8",
1843
- cwd,
1844
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1845
- stdio: ["pipe", "pipe", "pipe"]
1846
- });
1847
- return raw.split("\0").map((s) => s.trim()).filter(Boolean);
1848
- } catch {
1849
- return [];
1850
- }
1851
- }
1852
- function normalizeCommitMessage(raw) {
1853
- const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
1854
- if (!trimmed) return "chore: kody update";
1855
- const firstLine2 = trimmed.split("\n")[0];
1856
- for (const prefix of CONVENTIONAL_PREFIXES) {
1857
- if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
1858
- }
1859
- return `chore: ${trimmed}`;
1860
- }
1861
- function commitAndPush(branch, agentMessage, cwd) {
1862
- const allChanged = listChangedFiles(cwd);
1863
- const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1864
- const mergeHeadExists = fs11.existsSync(path10.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1865
- if (allowedFiles.length === 0 && !mergeHeadExists) {
1866
- return { committed: false, pushed: false, sha: "", message: "" };
1867
- }
1868
- for (const f of allowedFiles) {
1869
- try {
1870
- git(["add", "--", f], cwd);
1871
- } catch {
1872
- }
1873
- }
1874
- const message = normalizeCommitMessage(agentMessage);
1875
- try {
1876
- git(["commit", "--no-gpg-sign", "-m", message], cwd);
1877
- } catch (err) {
1878
- const msg = err instanceof Error ? err.message : String(err);
1879
- if (/nothing to commit/i.test(msg)) {
1880
- return { committed: false, pushed: false, sha: "", message };
1881
- }
1882
- throw err;
1883
- }
1884
- const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
1885
- try {
1886
- git(["push", "-u", "origin", branch], cwd);
1887
- } catch {
1888
- git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
1889
- }
1890
- return { committed: true, pushed: true, sha, message };
1891
- }
1892
- function hasCommitsAhead(branch, defaultBranch, cwd) {
1893
- try {
1894
- const out = git(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
1895
- return parseInt(out, 10) > 0;
1896
- } catch {
1897
- try {
1898
- const out = git(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
1899
- return parseInt(out, 10) > 0;
1900
- } catch {
1901
- return false;
1902
- }
1903
- }
1904
- }
1905
-
1906
- // src/scripts/commitAndPush.ts
1907
- var commitAndPush2 = async (ctx, profile) => {
1914
+ var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
1915
+ var commitAndPush2 = async (ctx) => {
1908
1916
  const branch = ctx.data.branch;
1909
1917
  if (!branch) {
1910
1918
  ctx.data.commitResult = { committed: false, pushed: false };
@@ -1915,21 +1923,7 @@ var commitAndPush2 = async (ctx, profile) => {
1915
1923
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
1916
1924
  return;
1917
1925
  }
1918
- const kind = profile.name;
1919
- if (kind === "resolve") {
1920
- try {
1921
- execFileSync6("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
1922
- } catch {
1923
- }
1924
- } else {
1925
- const aborted = abortUnfinishedGitOps(ctx.cwd);
1926
- if (aborted.length > 0) {
1927
- process.stderr.write(`[kody] cleaned up unfinished git ops: ${aborted.join(", ")}
1928
- `);
1929
- }
1930
- }
1931
- const fallbackMsg = defaultCommitMessage(kind, ctx.data);
1932
- const message = ctx.data.commitMessage || fallbackMsg;
1926
+ const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
1933
1927
  try {
1934
1928
  const result = commitAndPush(branch, message, ctx.cwd);
1935
1929
  ctx.data.commitResult = result;
@@ -1941,20 +1935,6 @@ var commitAndPush2 = async (ctx, profile) => {
1941
1935
  }
1942
1936
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
1943
1937
  };
1944
- function defaultCommitMessage(mode, data) {
1945
- switch (mode) {
1946
- case "run":
1947
- return `chore: kody changes for #${data.commentTargetNumber}`;
1948
- case "fix":
1949
- return `chore(fix): kody fix for PR #${data.commentTargetNumber}`;
1950
- case "fix-ci":
1951
- return `fix(ci): kody fix-ci for PR #${data.commentTargetNumber}`;
1952
- case "resolve":
1953
- return `fix: resolve merge conflicts with ${data.baseBranch}`;
1954
- default:
1955
- return `chore: kody changes`;
1956
- }
1957
- }
1958
1938
 
1959
1939
  // src/scripts/composePrompt.ts
1960
1940
  import * as fs12 from "fs";
@@ -2055,7 +2035,7 @@ function formatToolsUsage(profile) {
2055
2035
  }
2056
2036
 
2057
2037
  // src/scripts/diagMcp.ts
2058
- import { execFileSync as execFileSync7 } from "child_process";
2038
+ import { execFileSync as execFileSync6 } from "child_process";
2059
2039
  import * as fs13 from "fs";
2060
2040
  import * as os3 from "os";
2061
2041
  import * as path12 from "path";
@@ -2075,7 +2055,7 @@ var diagMcp = async (_ctx) => {
2075
2055
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
2076
2056
  `);
2077
2057
  try {
2078
- const v = execFileSync7(
2058
+ const v = execFileSync6(
2079
2059
  "npx",
2080
2060
  ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"],
2081
2061
  { stdio: "pipe", timeout: 6e4, encoding: "utf8" }
@@ -2575,7 +2555,7 @@ var discoverQaContext = async (ctx) => {
2575
2555
  };
2576
2556
 
2577
2557
  // src/scripts/dispatch.ts
2578
- import { execFileSync as execFileSync8 } from "child_process";
2558
+ import { execFileSync as execFileSync7 } from "child_process";
2579
2559
  var API_TIMEOUT_MS3 = 3e4;
2580
2560
  var dispatch = async (ctx, _profile, _agentResult, args) => {
2581
2561
  const next = args?.next;
@@ -2598,7 +2578,7 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
2598
2578
  const sub = usePr ? "pr" : "issue";
2599
2579
  const body = `@kody ${next}`;
2600
2580
  try {
2601
- execFileSync8("gh", [sub, "comment", String(targetNumber), "--body", body], {
2581
+ execFileSync7("gh", [sub, "comment", String(targetNumber), "--body", body], {
2602
2582
  timeout: API_TIMEOUT_MS3,
2603
2583
  cwd: ctx.cwd,
2604
2584
  stdio: ["ignore", "pipe", "pipe"]
@@ -2618,7 +2598,7 @@ function parsePr(url) {
2618
2598
  }
2619
2599
 
2620
2600
  // src/issue.ts
2621
- import { execFileSync as execFileSync9 } from "child_process";
2601
+ import { execFileSync as execFileSync8 } from "child_process";
2622
2602
  var API_TIMEOUT_MS4 = 3e4;
2623
2603
  function ghToken2() {
2624
2604
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -2626,7 +2606,7 @@ function ghToken2() {
2626
2606
  function gh2(args, options) {
2627
2607
  const token = ghToken2();
2628
2608
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2629
- return execFileSync9("gh", args, {
2609
+ return execFileSync8("gh", args, {
2630
2610
  encoding: "utf-8",
2631
2611
  timeout: API_TIMEOUT_MS4,
2632
2612
  cwd: options?.cwd,
@@ -2752,6 +2732,70 @@ function postPrReviewComment(prNumber, body, cwd) {
2752
2732
  }
2753
2733
  }
2754
2734
 
2735
+ // src/scripts/dispatchManagerTicks.ts
2736
+ var dispatchManagerTicks = async (ctx, _profile, args) => {
2737
+ ctx.skipAgent = true;
2738
+ const label = String(args?.label ?? "");
2739
+ const targetExecutable = String(args?.targetExecutable ?? "");
2740
+ if (!label) throw new Error("dispatchManagerTicks: `with.label` is required");
2741
+ if (!targetExecutable) throw new Error("dispatchManagerTicks: `with.targetExecutable` is required");
2742
+ const issueArg = String(args?.issueArg ?? "issue");
2743
+ const issues = listIssuesByLabel(label, ctx.cwd);
2744
+ ctx.data.managerIssueCount = issues.length;
2745
+ if (issues.length === 0) {
2746
+ process.stdout.write(`[manager] no open issues with label "${label}"
2747
+ `);
2748
+ return;
2749
+ }
2750
+ process.stdout.write(`[manager] ticking ${issues.length} issue(s) via ${targetExecutable}
2751
+ `);
2752
+ const results = [];
2753
+ for (const issue of issues) {
2754
+ process.stdout.write(`[manager] \u2192 tick #${issue.number}: ${issue.title}
2755
+ `);
2756
+ try {
2757
+ const out = await runExecutable(targetExecutable, {
2758
+ cliArgs: { [issueArg]: issue.number },
2759
+ cwd: ctx.cwd,
2760
+ config: ctx.config,
2761
+ verbose: ctx.verbose,
2762
+ quiet: ctx.quiet
2763
+ });
2764
+ results.push({ issue: issue.number, exitCode: out.exitCode, reason: out.reason });
2765
+ if (out.exitCode !== 0) {
2766
+ process.stderr.write(`[manager] tick #${issue.number} failed (exit ${out.exitCode}): ${out.reason ?? ""}
2767
+ `);
2768
+ }
2769
+ } catch (err) {
2770
+ const msg = err instanceof Error ? err.message : String(err);
2771
+ process.stderr.write(`[manager] tick #${issue.number} crashed: ${msg}
2772
+ `);
2773
+ results.push({ issue: issue.number, exitCode: 99, reason: msg });
2774
+ }
2775
+ }
2776
+ ctx.data.managerTickResults = results;
2777
+ ctx.output.exitCode = 0;
2778
+ };
2779
+ function listIssuesByLabel(label, cwd) {
2780
+ let raw = "";
2781
+ try {
2782
+ raw = gh2(
2783
+ ["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"],
2784
+ { cwd }
2785
+ );
2786
+ } catch {
2787
+ return [];
2788
+ }
2789
+ let list;
2790
+ try {
2791
+ list = JSON.parse(raw);
2792
+ } catch {
2793
+ return [];
2794
+ }
2795
+ if (!Array.isArray(list)) return [];
2796
+ return list.filter((x) => typeof x.number === "number" && typeof x.title === "string").map((x) => ({ number: x.number, title: x.title }));
2797
+ }
2798
+
2755
2799
  // src/pr.ts
2756
2800
  var TITLE_MAX = 72;
2757
2801
  function stripTitlePrefixes(raw) {
@@ -2930,7 +2974,7 @@ function computeFailureReason(ctx) {
2930
2974
  }
2931
2975
 
2932
2976
  // src/scripts/finishFlow.ts
2933
- import { execFileSync as execFileSync10 } from "child_process";
2977
+ import { execFileSync as execFileSync9 } from "child_process";
2934
2978
 
2935
2979
  // src/lifecycleLabels.ts
2936
2980
  var KODY_NAMESPACE = "kody";
@@ -3089,7 +3133,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
3089
3133
  **PR:** ${state.core.prUrl}` : "";
3090
3134
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
3091
3135
  try {
3092
- execFileSync10("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3136
+ execFileSync9("gh", ["issue", "comment", String(issueNumber), "--body", body], {
3093
3137
  timeout: API_TIMEOUT_MS5,
3094
3138
  cwd: ctx.cwd,
3095
3139
  stdio: ["ignore", "pipe", "pipe"]
@@ -3103,7 +3147,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
3103
3147
  };
3104
3148
 
3105
3149
  // src/branch.ts
3106
- import { execFileSync as execFileSync11 } from "child_process";
3150
+ import { execFileSync as execFileSync10 } from "child_process";
3107
3151
  var UncommittedChangesError = class extends Error {
3108
3152
  constructor(branch) {
3109
3153
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -3113,7 +3157,7 @@ var UncommittedChangesError = class extends Error {
3113
3157
  branch;
3114
3158
  };
3115
3159
  function git2(args, cwd) {
3116
- return execFileSync11("git", args, {
3160
+ return execFileSync10("git", args, {
3117
3161
  encoding: "utf-8",
3118
3162
  timeout: 3e4,
3119
3163
  cwd,
@@ -3138,7 +3182,7 @@ function checkoutPrBranch(prNumber, cwd) {
3138
3182
  SKIP_HOOKS: "1",
3139
3183
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
3140
3184
  };
3141
- execFileSync11("gh", ["pr", "checkout", String(prNumber)], {
3185
+ execFileSync10("gh", ["pr", "checkout", String(prNumber)], {
3142
3186
  cwd,
3143
3187
  env,
3144
3188
  stdio: ["ignore", "pipe", "pipe"],
@@ -3205,7 +3249,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3205
3249
  }
3206
3250
 
3207
3251
  // src/gha.ts
3208
- import { execFileSync as execFileSync12 } from "child_process";
3252
+ import { execFileSync as execFileSync11 } from "child_process";
3209
3253
  import * as fs16 from "fs";
3210
3254
  function getRunUrl() {
3211
3255
  const server = process.env.GITHUB_SERVER_URL;
@@ -3248,7 +3292,7 @@ function reactToTriggerComment(cwd) {
3248
3292
  for (let attempt = 0; attempt < 3; attempt++) {
3249
3293
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
3250
3294
  try {
3251
- execFileSync12("gh", args, opts);
3295
+ execFileSync11("gh", args, opts);
3252
3296
  return;
3253
3297
  } catch (err) {
3254
3298
  lastErr = err;
@@ -3261,13 +3305,13 @@ function reactToTriggerComment(cwd) {
3261
3305
  }
3262
3306
  function sleepMs(ms) {
3263
3307
  try {
3264
- execFileSync12("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3308
+ execFileSync11("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
3265
3309
  } catch {
3266
3310
  }
3267
3311
  }
3268
3312
 
3269
3313
  // src/workflow.ts
3270
- import { execFileSync as execFileSync13 } from "child_process";
3314
+ import { execFileSync as execFileSync12 } from "child_process";
3271
3315
  var GH_TIMEOUT_MS = 3e4;
3272
3316
  function ghToken3() {
3273
3317
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -3275,7 +3319,7 @@ function ghToken3() {
3275
3319
  function gh3(args, cwd) {
3276
3320
  const token = ghToken3();
3277
3321
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3278
- return execFileSync13("gh", args, {
3322
+ return execFileSync12("gh", args, {
3279
3323
  encoding: "utf-8",
3280
3324
  timeout: GH_TIMEOUT_MS,
3281
3325
  cwd,
@@ -3459,7 +3503,7 @@ function tryPostPr2(prNumber, body, cwd) {
3459
3503
  }
3460
3504
 
3461
3505
  // src/scripts/initFlow.ts
3462
- import { execFileSync as execFileSync14 } from "child_process";
3506
+ import { execFileSync as execFileSync13 } from "child_process";
3463
3507
  import * as fs18 from "fs";
3464
3508
  import * as path16 from "path";
3465
3509
 
@@ -3500,7 +3544,7 @@ function qualityCommandsFor(pm) {
3500
3544
  function detectOwnerRepo(cwd) {
3501
3545
  let url;
3502
3546
  try {
3503
- url = execFileSync14("git", ["remote", "get-url", "origin"], {
3547
+ url = execFileSync13("git", ["remote", "get-url", "origin"], {
3504
3548
  cwd,
3505
3549
  encoding: "utf-8",
3506
3550
  stdio: ["ignore", "pipe", "pipe"]
@@ -3584,7 +3628,7 @@ jobs:
3584
3628
  `;
3585
3629
  function defaultBranchFromGit(cwd) {
3586
3630
  try {
3587
- const ref = execFileSync14("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3631
+ const ref = execFileSync13("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
3588
3632
  cwd,
3589
3633
  encoding: "utf-8",
3590
3634
  stdio: ["ignore", "pipe", "pipe"]
@@ -3592,7 +3636,7 @@ function defaultBranchFromGit(cwd) {
3592
3636
  return ref.replace("refs/remotes/origin/", "");
3593
3637
  } catch {
3594
3638
  try {
3595
- return execFileSync14("git", ["branch", "--show-current"], {
3639
+ return execFileSync13("git", ["branch", "--show-current"], {
3596
3640
  cwd,
3597
3641
  encoding: "utf-8",
3598
3642
  stdio: ["ignore", "pipe", "pipe"]
@@ -3764,6 +3808,112 @@ var loadIssueContext = async (ctx) => {
3764
3808
  ctx.data.commentTargetNumber = issueNumber;
3765
3809
  };
3766
3810
 
3811
+ // src/scripts/issueStateComment.ts
3812
+ function isStateEnvelope(x) {
3813
+ if (x === null || typeof x !== "object") return false;
3814
+ const o = x;
3815
+ return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
3816
+ }
3817
+ function formatStateCommentBody(marker, state) {
3818
+ return `<!-- ${marker} -->
3819
+
3820
+ \`\`\`json
3821
+ ${JSON.stringify(state, null, 2)}
3822
+ \`\`\`
3823
+ `;
3824
+ }
3825
+ function parseStateCommentBody(marker, body) {
3826
+ const markerLine = `<!-- ${marker} -->`;
3827
+ if (!body.trimStart().startsWith(markerLine)) return null;
3828
+ const fenceOpen = body.indexOf("```json");
3829
+ if (fenceOpen === -1) return null;
3830
+ const after = body.slice(fenceOpen + "```json".length);
3831
+ const fenceClose = after.indexOf("```");
3832
+ if (fenceClose === -1) return null;
3833
+ const jsonText = after.slice(0, fenceClose).trim();
3834
+ let parsed;
3835
+ try {
3836
+ parsed = JSON.parse(jsonText);
3837
+ } catch {
3838
+ return null;
3839
+ }
3840
+ return isStateEnvelope(parsed) ? parsed : null;
3841
+ }
3842
+ function listIssueComments(owner, repo, issueNumber, cwd) {
3843
+ const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
3844
+ let parsed;
3845
+ try {
3846
+ parsed = JSON.parse(raw);
3847
+ } catch {
3848
+ return [];
3849
+ }
3850
+ if (!Array.isArray(parsed)) return [];
3851
+ return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
3852
+ }
3853
+ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
3854
+ const comments = listIssueComments(owner, repo, issueNumber, cwd);
3855
+ for (const c of comments) {
3856
+ const state = parseStateCommentBody(marker, c.body);
3857
+ if (!state) continue;
3858
+ return { commentId: c.id, commentNodeId: c.node_id, state };
3859
+ }
3860
+ return null;
3861
+ }
3862
+ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3863
+ const body = formatStateCommentBody(marker, state);
3864
+ const raw = gh2(
3865
+ ["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"],
3866
+ { cwd, input: JSON.stringify({ body }) }
3867
+ );
3868
+ const parsed = JSON.parse(raw);
3869
+ try {
3870
+ minimizeComment(parsed.node_id, cwd);
3871
+ } catch {
3872
+ }
3873
+ return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
3874
+ }
3875
+ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
3876
+ const body = formatStateCommentBody(marker, state);
3877
+ gh2(
3878
+ ["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"],
3879
+ { cwd, input: JSON.stringify({ body }) }
3880
+ );
3881
+ try {
3882
+ minimizeComment(commentNodeId, cwd);
3883
+ } catch {
3884
+ }
3885
+ }
3886
+ function minimizeComment(nodeId, cwd) {
3887
+ const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
3888
+ gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
3889
+ }
3890
+
3891
+ // src/scripts/loadIssueStateComment.ts
3892
+ var loadIssueStateComment = async (ctx, _profile, args) => {
3893
+ const marker = String(args?.marker ?? "");
3894
+ if (!marker) {
3895
+ throw new Error("loadIssueStateComment: `with.marker` is required");
3896
+ }
3897
+ const issueArg = String(args?.issueArg ?? "issue");
3898
+ const issueNumber = Number(ctx.args[issueArg]);
3899
+ if (!Number.isFinite(issueNumber) || issueNumber <= 0) {
3900
+ throw new Error(`loadIssueStateComment: ctx.args.${issueArg} must be a positive integer`);
3901
+ }
3902
+ const owner = ctx.config.github.owner;
3903
+ const repo = ctx.config.github.repo;
3904
+ if (!owner || !repo) {
3905
+ throw new Error("loadIssueStateComment: ctx.config.github.owner/repo must be set");
3906
+ }
3907
+ const issue = getIssue(issueNumber, ctx.cwd);
3908
+ const loaded = findStateComment2(owner, repo, issueNumber, marker, ctx.cwd);
3909
+ ctx.data.stateMarker = marker;
3910
+ ctx.data.issueIntent = issue.body;
3911
+ ctx.data.issueTitle = issue.title;
3912
+ ctx.data.issueNumber = String(issueNumber);
3913
+ ctx.data.issueStateComment = loaded;
3914
+ ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
3915
+ };
3916
+
3767
3917
  // src/scripts/loadPriorArt.ts
3768
3918
  var PER_PR_DIFF_MAX_BYTES = 8e3;
3769
3919
  var TOTAL_MAX_BYTES = 3e4;
@@ -3931,6 +4081,53 @@ function makeAction(type, payload) {
3931
4081
  return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3932
4082
  }
3933
4083
 
4084
+ // src/scripts/parseIssueStateFromAgentResult.ts
4085
+ function isPartialEnvelope(x) {
4086
+ if (x === null || typeof x !== "object") return false;
4087
+ const o = x;
4088
+ return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
4089
+ }
4090
+ var parseIssueStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
4091
+ const fenceLabel = String(args?.fenceLabel ?? "");
4092
+ if (!fenceLabel) {
4093
+ throw new Error("parseIssueStateFromAgentResult: `with.fenceLabel` is required");
4094
+ }
4095
+ if (!agentResult) {
4096
+ ctx.data.nextStateParseError = "agent did not run";
4097
+ return;
4098
+ }
4099
+ const fenceRegex = new RegExp("```" + escapeRegex(fenceLabel) + "\\s*\\n([\\s\\S]*?)\\n```", "m");
4100
+ const match = fenceRegex.exec(agentResult.finalText);
4101
+ if (!match) {
4102
+ ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
4103
+ return;
4104
+ }
4105
+ let parsed;
4106
+ try {
4107
+ parsed = JSON.parse(match[1].trim());
4108
+ } catch (err) {
4109
+ ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
4110
+ return;
4111
+ }
4112
+ if (!isPartialEnvelope(parsed)) {
4113
+ ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
4114
+ return;
4115
+ }
4116
+ const loaded = ctx.data.issueStateComment;
4117
+ const prevRev = loaded?.state.rev ?? 0;
4118
+ const next = {
4119
+ version: 1,
4120
+ rev: prevRev + 1,
4121
+ cursor: parsed.cursor,
4122
+ data: parsed.data,
4123
+ done: parsed.done
4124
+ };
4125
+ ctx.data.nextIssueState = next;
4126
+ };
4127
+ function escapeRegex(s) {
4128
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4129
+ }
4130
+
3934
4131
  // src/scripts/persistArtifacts.ts
3935
4132
  var persistArtifacts = async (ctx, profile) => {
3936
4133
  if (profile.outputArtifacts.length === 0) return;
@@ -3977,7 +4174,7 @@ var persistFlowState = async (ctx) => {
3977
4174
  };
3978
4175
 
3979
4176
  // src/scripts/postClassification.ts
3980
- import { execFileSync as execFileSync15 } from "child_process";
4177
+ import { execFileSync as execFileSync14 } from "child_process";
3981
4178
  var API_TIMEOUT_MS6 = 3e4;
3982
4179
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3983
4180
  var postClassification = async (ctx) => {
@@ -4007,7 +4204,7 @@ var postClassification = async (ctx) => {
4007
4204
  ctx.cwd
4008
4205
  );
4009
4206
  try {
4010
- execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4207
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4011
4208
  cwd: ctx.cwd,
4012
4209
  timeout: API_TIMEOUT_MS6,
4013
4210
  stdio: ["ignore", "pipe", "pipe"]
@@ -4041,7 +4238,7 @@ function parseClassification(prSummary) {
4041
4238
  }
4042
4239
  function tryAuditComment(issueNumber, body, cwd) {
4043
4240
  try {
4044
- execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4241
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4045
4242
  cwd,
4046
4243
  timeout: API_TIMEOUT_MS6,
4047
4244
  stdio: ["ignore", "pipe", "pipe"]
@@ -4225,7 +4422,7 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
4225
4422
  };
4226
4423
 
4227
4424
  // src/scripts/releaseFlow.ts
4228
- import { execFileSync as execFileSync16, spawnSync } from "child_process";
4425
+ import { execFileSync as execFileSync15, spawnSync } from "child_process";
4229
4426
  import * as fs19 from "fs";
4230
4427
  import * as path17 from "path";
4231
4428
  function notifyIssue(issueNumber, body, cwd) {
@@ -4265,7 +4462,7 @@ function generateChangelog(cwd, newVersion, lastTag) {
4265
4462
  else logArgs.splice(1, 0, `-n${FIRST_RELEASE_COMMIT_CAP}`, "HEAD");
4266
4463
  let log = "";
4267
4464
  try {
4268
- log = execFileSync16("git", logArgs, {
4465
+ log = execFileSync15("git", logArgs, {
4269
4466
  cwd,
4270
4467
  encoding: "utf-8",
4271
4468
  stdio: ["ignore", "pipe", "pipe"]
@@ -4322,7 +4519,7 @@ ${entry}${prior.slice(idx + 1)}`);
4322
4519
  }
4323
4520
  }
4324
4521
  function git3(args, cwd, timeout = 6e4) {
4325
- return execFileSync16("git", args, {
4522
+ return execFileSync15("git", args, {
4326
4523
  encoding: "utf-8",
4327
4524
  timeout,
4328
4525
  cwd,
@@ -4699,7 +4896,7 @@ var resolveArtifacts = async (ctx, profile) => {
4699
4896
  };
4700
4897
 
4701
4898
  // src/scripts/resolveFlow.ts
4702
- import { execFileSync as execFileSync17 } from "child_process";
4899
+ import { execFileSync as execFileSync16 } from "child_process";
4703
4900
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
4704
4901
  var resolveFlow = async (ctx) => {
4705
4902
  const prNumber = ctx.args.pr;
@@ -4769,7 +4966,7 @@ function buildPreferBlock(prefer, baseBranch) {
4769
4966
  }
4770
4967
  function getConflictedFiles(cwd) {
4771
4968
  try {
4772
- const out = execFileSync17("git", ["diff", "--name-only", "--diff-filter=U"], {
4969
+ const out = execFileSync16("git", ["diff", "--name-only", "--diff-filter=U"], {
4773
4970
  encoding: "utf-8",
4774
4971
  cwd,
4775
4972
  env: { ...process.env, HUSKY: "0" }
@@ -4784,7 +4981,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
4784
4981
  let total = 0;
4785
4982
  for (const f of files) {
4786
4983
  try {
4787
- const content = execFileSync17("cat", [f], { encoding: "utf-8", cwd }).toString();
4984
+ const content = execFileSync16("cat", [f], { encoding: "utf-8", cwd }).toString();
4788
4985
  const snippet = `### ${f}
4789
4986
 
4790
4987
  \`\`\`
@@ -4947,6 +5144,20 @@ var skipAgent = async (ctx) => {
4947
5144
  ctx.skipAgent = true;
4948
5145
  };
4949
5146
 
5147
+ // src/scripts/stageMergeConflicts.ts
5148
+ import { execFileSync as execFileSync17 } from "child_process";
5149
+ var stageMergeConflicts = async (ctx) => {
5150
+ if (ctx.data.agentDone === false) return;
5151
+ try {
5152
+ execFileSync17("git", ["add", "-A"], {
5153
+ cwd: ctx.cwd,
5154
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
5155
+ stdio: "pipe"
5156
+ });
5157
+ } catch {
5158
+ }
5159
+ };
5160
+
4950
5161
  // src/scripts/startFlow.ts
4951
5162
  import { execFileSync as execFileSync18 } from "child_process";
4952
5163
  var API_TIMEOUT_MS7 = 3e4;
@@ -5238,6 +5449,42 @@ var watchStalePrsFlow = async (ctx) => {
5238
5449
  ctx.data.staleCount = stale.length;
5239
5450
  };
5240
5451
 
5452
+ // src/scripts/writeIssueStateComment.ts
5453
+ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
5454
+ const marker = String(args?.marker ?? "");
5455
+ if (!marker) {
5456
+ throw new Error("writeIssueStateComment: `with.marker` is required");
5457
+ }
5458
+ const issueArg = String(args?.issueArg ?? "issue");
5459
+ const issueNumber = Number(ctx.args[issueArg]);
5460
+ if (!Number.isFinite(issueNumber) || issueNumber <= 0) {
5461
+ throw new Error(`writeIssueStateComment: ctx.args.${issueArg} must be a positive integer`);
5462
+ }
5463
+ const parseError = ctx.data.nextStateParseError;
5464
+ if (parseError) {
5465
+ process.stderr.write(`[kody] state write skipped: ${parseError}
5466
+ `);
5467
+ if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
5468
+ if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
5469
+ return;
5470
+ }
5471
+ const next = ctx.data.nextIssueState;
5472
+ if (!next) {
5473
+ return;
5474
+ }
5475
+ const owner = ctx.config.github.owner;
5476
+ const repo = ctx.config.github.repo;
5477
+ if (!owner || !repo) {
5478
+ throw new Error("writeIssueStateComment: ctx.config.github.owner/repo must be set");
5479
+ }
5480
+ const loaded = ctx.data.issueStateComment;
5481
+ if (loaded) {
5482
+ updateStateComment(owner, repo, loaded.commentId, loaded.commentNodeId, marker, next, ctx.cwd);
5483
+ } else {
5484
+ createStateComment(owner, repo, issueNumber, marker, next, ctx.cwd);
5485
+ }
5486
+ };
5487
+
5241
5488
  // src/scripts/writeRunSummary.ts
5242
5489
  import * as fs20 from "fs";
5243
5490
  var writeRunSummary = async (ctx, profile) => {
@@ -5280,6 +5527,7 @@ var preflightScripts = {
5280
5527
  watchStalePrsFlow,
5281
5528
  loadTaskState,
5282
5529
  loadIssueContext,
5530
+ loadIssueStateComment,
5283
5531
  loadConventions,
5284
5532
  loadCoverageRules,
5285
5533
  loadPriorArt,
@@ -5292,14 +5540,19 @@ var preflightScripts = {
5292
5540
  setLifecycleLabel,
5293
5541
  skipAgent,
5294
5542
  classifyByLabel,
5295
- diagMcp
5543
+ diagMcp,
5544
+ dispatchManagerTicks
5296
5545
  };
5297
5546
  var postflightScripts = {
5298
5547
  parseAgentResult: parseAgentResult2,
5548
+ parseIssueStateFromAgentResult,
5549
+ writeIssueStateComment,
5299
5550
  requireFeedbackActions,
5300
5551
  requirePlanDeviations,
5301
5552
  verify,
5302
5553
  checkCoverageWithRetry,
5554
+ abortUnfinishedGitOps: abortUnfinishedGitOps2,
5555
+ stageMergeConflicts,
5303
5556
  commitAndPush: commitAndPush2,
5304
5557
  ensurePr: ensurePr2,
5305
5558
  postIssueComment: postIssueComment2,