xab 5.0.0 → 7.0.0

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.
Files changed (2) hide show
  1. package/dist/index.js +85 -44
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ var init_config = __esm(() => {
43
43
  promptHints: [],
44
44
  pathRemaps: [],
45
45
  reviewStrictness: "normal",
46
- maxAttempts: 2,
46
+ maxAttempts: undefined,
47
47
  commitPrefix: "backmerge:"
48
48
  };
49
49
  });
@@ -543,7 +543,9 @@ After making all file changes, you MUST run these two commands as your FINAL act
543
543
  If you do not run both commands, your work will be discarded. This is not optional.
544
544
  The validation system checks for exactly one new git commit. Zero commits = failure.
545
545
 
546
- Report what you did.`;
546
+ Report what you did.
547
+
548
+ MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
547
549
  const firstPrompt = `${MERGE_PREAMBLE}
548
550
  ${opts.repoContext ? `## Repository context
549
551
  ${opts.repoContext}
@@ -622,7 +624,9 @@ After making all fixes, you MUST run these two commands as your FINAL action:
622
624
 
623
625
  If you do not run both commands, your fixes will be discarded. This is not optional.
624
626
 
625
- Report what you fixed.`;
627
+ Report what you fixed.
628
+
629
+ MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
626
630
  const turn = await thread.run(prompt, { outputSchema: applyResultSchema });
627
631
  return parseJson(turn.finalResponse, {
628
632
  applied: false,
@@ -783,7 +787,7 @@ If you have ANY objections, be specific about what's wrong and how to fix it. Yo
783
787
  options: {
784
788
  cwd: worktreePath,
785
789
  model: "claude-opus-4-6",
786
- maxTurns: 30,
790
+ maxTurns: undefined,
787
791
  permissionMode: "default",
788
792
  outputFormat: reviewSchema,
789
793
  tools: ["Read", "Glob", "Grep", "Bash"],
@@ -1173,37 +1177,24 @@ async function validateApply(worktreeGit, beforeHash) {
1173
1177
  errors.push(`Failed to check commits: ${e.message}`);
1174
1178
  }
1175
1179
  const status = await worktreeGit.status();
1176
- const isOurs = (f) => f.startsWith(".backmerge/") || f.startsWith(".backmerge\\");
1177
- const modified = status.modified.filter((f) => !isOurs(f));
1178
- const created = status.created.filter((f) => !isOurs(f));
1179
- const deleted = status.deleted.filter((f) => !isOurs(f));
1180
- const notAdded = status.not_added.filter((f) => !isOurs(f));
1181
- const conflicted = status.conflicted.filter((f) => !isOurs(f));
1180
+ const isInfra = (f) => f.startsWith(".backmerge/") || f.startsWith(".backmerge\\") || f.startsWith(".git-local/") || f.startsWith(".git-local\\");
1181
+ const modified = status.modified.filter((f) => !isInfra(f));
1182
+ const created = status.created.filter((f) => !isInfra(f));
1183
+ const deleted = status.deleted.filter((f) => !isInfra(f));
1184
+ const notAdded = status.not_added.filter((f) => !isInfra(f));
1185
+ const conflicted = status.conflicted.filter((f) => !isInfra(f));
1182
1186
  const worktreeClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && conflicted.length === 0 && notAdded.length === 0;
1183
1187
  const dirtyFiles = [];
1184
1188
  if (!worktreeClean) {
1185
- const parts = [];
1186
- if (modified.length) {
1187
- parts.push(`${modified.length} modified`);
1188
- for (const f of modified)
1189
- dirtyFiles.push(`M ${f}`);
1190
- }
1191
- if (notAdded.length) {
1192
- parts.push(`${notAdded.length} untracked`);
1193
- for (const f of notAdded)
1194
- dirtyFiles.push(`? ${f}`);
1195
- }
1196
- if (deleted.length) {
1197
- parts.push(`${deleted.length} deleted`);
1198
- for (const f of deleted)
1199
- dirtyFiles.push(`D ${f}`);
1200
- }
1201
- if (conflicted.length) {
1202
- parts.push(`${conflicted.length} conflicted`);
1203
- for (const f of conflicted)
1204
- dirtyFiles.push(`C ${f}`);
1205
- }
1206
- errors.push(`Working tree not clean: ${parts.join(", ")}`);
1189
+ for (const f of modified)
1190
+ dirtyFiles.push(`M ${f}`);
1191
+ for (const f of notAdded)
1192
+ dirtyFiles.push(`? ${f}`);
1193
+ for (const f of deleted)
1194
+ dirtyFiles.push(`D ${f}`);
1195
+ for (const f of conflicted)
1196
+ dirtyFiles.push(`C ${f}`);
1197
+ errors.push(`Working tree not clean (${dirtyFiles.length} files): ${dirtyFiles.join(", ")}`);
1207
1198
  }
1208
1199
  const conflictMarkers = [];
1209
1200
  if (newCommitHash) {
@@ -1241,7 +1232,7 @@ async function getAppliedDiffStat(worktreeGit, beforeHash) {
1241
1232
  }
1242
1233
  async function resetHard(git, ref) {
1243
1234
  await git.raw(["reset", "--hard", ref]);
1244
- await git.raw(["clean", "-fd", "--exclude=.backmerge"]);
1235
+ await git.raw(["clean", "-fd", "--exclude=.backmerge", "--exclude=.git-local"]);
1245
1236
  }
1246
1237
  async function fetchOrigin(git) {
1247
1238
  await git.fetch(["--all", "--prune"]);
@@ -1281,7 +1272,7 @@ async function runEngine(opts, cb) {
1281
1272
  autoSkip = true
1282
1273
  } = opts;
1283
1274
  const config = loadConfig(repoPath, opts.configPath);
1284
- const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ?? 2;
1275
+ const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ?? Infinity;
1285
1276
  const effectiveWorkBranch = opts.workBranch ?? config.workBranch;
1286
1277
  const commitPrefix = config.commitPrefix ?? "backmerge:";
1287
1278
  if (!checkCodexInstalled())
@@ -1561,7 +1552,7 @@ async function processOneCommit(o) {
1561
1552
  }
1562
1553
  for (let attempt = 1;attempt <= o.maxAttempts; attempt++) {
1563
1554
  const headBefore = await getHead(o.wtGit);
1564
- cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt}/${o.maxAttempts})...`);
1555
+ cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt})...`);
1565
1556
  let applyResult;
1566
1557
  try {
1567
1558
  applyResult = await applyCommit({
@@ -1584,7 +1575,26 @@ async function processOneCommit(o) {
1584
1575
  continue;
1585
1576
  }
1586
1577
  cb.onStatus(`Validating ${commit.hash.slice(0, 8)}...`);
1587
- const validation = await validateApply(o.wtGit, headBefore);
1578
+ let validation = await validateApply(o.wtGit, headBefore);
1579
+ if (!validation.valid && validation.newCommitCount === 0 && !validation.worktreeClean && validation.dirtyFiles.length > 0) {
1580
+ const realChanges = validation.dirtyFiles.filter((f) => !f.includes(".git-local/"));
1581
+ if (realChanges.length > 0) {
1582
+ cb.onLog(`Codex left ${realChanges.length} changed files without committing \u2014 creating rescue commit`, "yellow");
1583
+ for (const f of realChanges)
1584
+ cb.onLog(` ${f}`, "yellow");
1585
+ const commitMsg = `${o.commitPrefix} ${commit.message} (from ${commit.hash.slice(0, 8)})`;
1586
+ try {
1587
+ await o.wtGit.raw(["add", "-A", "--", ".", ":!.git-local"]);
1588
+ await o.wtGit.raw(["commit", "-m", commitMsg]);
1589
+ validation = await validateApply(o.wtGit, headBefore);
1590
+ if (validation.valid) {
1591
+ cb.onLog(`Rescue commit succeeded`, "green");
1592
+ }
1593
+ } catch (e) {
1594
+ cb.onLog(`Rescue commit failed: ${e.message}`, "red");
1595
+ }
1596
+ }
1597
+ }
1588
1598
  if (!validation.valid) {
1589
1599
  cb.onLog(`Validation failed: ${validation.errors.join("; ")}`, "red");
1590
1600
  if (validation.dirtyFiles.length > 0) {
@@ -1638,10 +1648,10 @@ async function processOneCommit(o) {
1638
1648
  }
1639
1649
  if (!reviewResult.approved) {
1640
1650
  cb.onLog(`Review rejected: ${reviewResult.issues.join("; ")}`, "red");
1641
- const maxFixRounds = 2;
1651
+ const maxFixRounds = Infinity;
1642
1652
  let fixed = false;
1643
1653
  for (let fixRound = 1;fixRound <= maxFixRounds; fixRound++) {
1644
- cb.onStatus(`Codex fixing review issues (round ${fixRound}/${maxFixRounds})...`);
1654
+ cb.onStatus(`Codex fixing review issues (round ${fixRound})...`);
1645
1655
  try {
1646
1656
  await fixFromReview({
1647
1657
  worktreePath: o.wtPath,
@@ -1703,7 +1713,7 @@ async function processOneCommit(o) {
1703
1713
  kind: "failed",
1704
1714
  commitHash: commit.hash,
1705
1715
  commitMessage: commit.message,
1706
- reason: `Review rejected after ${maxFixRounds} fix rounds: ${reviewResult.issues.join("; ")}`,
1716
+ reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
1707
1717
  failedPhase: "review",
1708
1718
  reviewApproved: false,
1709
1719
  reviewIssues: reviewResult.issues,
@@ -1717,7 +1727,7 @@ async function processOneCommit(o) {
1717
1727
  kind: "failed",
1718
1728
  commitHash: commit.hash,
1719
1729
  commitMessage: commit.message,
1720
- reason: `Review rejected after ${maxFixRounds} fix rounds: ${reviewResult.issues.join("; ")}`,
1730
+ reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
1721
1731
  failedPhase: "review",
1722
1732
  reviewApproved: false,
1723
1733
  reviewIssues: reviewResult.issues,
@@ -1791,6 +1801,16 @@ __export(exports_batch, {
1791
1801
  runBatch: () => runBatch
1792
1802
  });
1793
1803
  import chalk from "chalk";
1804
+ import { readFileSync as readFileSync5 } from "fs";
1805
+ import { join as join6 } from "path";
1806
+ function getVersion() {
1807
+ try {
1808
+ const pkg = JSON.parse(readFileSync5(join6(import.meta.dir, "..", "package.json"), "utf-8"));
1809
+ return pkg.version ?? "?";
1810
+ } catch {
1811
+ return "?";
1812
+ }
1813
+ }
1794
1814
  function shortHash2(h) {
1795
1815
  return h.slice(0, 8);
1796
1816
  }
@@ -1846,8 +1866,9 @@ function emitJsonl(obj) {
1846
1866
  async function runBatch(opts) {
1847
1867
  const jsonl = opts.jsonl ?? false;
1848
1868
  const startTime = Date.now();
1869
+ const version = getVersion();
1849
1870
  log("");
1850
- log(` ${chalk.cyan.bold("xab")} ${chalk.dim("\u2014 curated branch reconciliation")}`);
1871
+ log(` ${chalk.cyan.bold("xab")} ${chalk.dim(`v${version}`)} ${chalk.dim("\u2014 curated branch reconciliation")}`);
1851
1872
  log(` ${chalk.magenta(opts.sourceRef)} ${chalk.dim("\u2192")} ${chalk.green(opts.targetRef)}`);
1852
1873
  if (opts.workBranch)
1853
1874
  log(` ${chalk.dim("work branch:")} ${chalk.cyan(opts.workBranch)}`);
@@ -2041,7 +2062,16 @@ import { useState, useEffect, useCallback, useRef } from "react";
2041
2062
  import { Box, Text, useInput, useApp, Static, Newline } from "ink";
2042
2063
  import SelectInput from "ink-select-input";
2043
2064
  import Spinner from "ink-spinner";
2065
+ import { readFileSync as readFileSync4 } from "fs";
2066
+ import { join as join5 } from "path";
2044
2067
  import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
2068
+ var XAB_VERSION = (() => {
2069
+ try {
2070
+ return JSON.parse(readFileSync4(join5(import.meta.dir, "..", "package.json"), "utf-8")).version ?? "?";
2071
+ } catch {
2072
+ return "?";
2073
+ }
2074
+ })();
2045
2075
  function shortHash(h) {
2046
2076
  return h.slice(0, 8);
2047
2077
  }
@@ -2066,8 +2096,15 @@ function Header({
2066
2096
  /* @__PURE__ */ jsxDEV(Text, {
2067
2097
  bold: true,
2068
2098
  color: "cyan",
2069
- children: "\u256D\u2500 backmerge"
2099
+ children: "\u256D\u2500 xab"
2070
2100
  }, undefined, false, undefined, this),
2101
+ /* @__PURE__ */ jsxDEV(Text, {
2102
+ dimColor: true,
2103
+ children: [
2104
+ " v",
2105
+ XAB_VERSION
2106
+ ]
2107
+ }, undefined, true, undefined, this),
2071
2108
  /* @__PURE__ */ jsxDEV(Text, {
2072
2109
  color: "gray",
2073
2110
  children: " \u2014 curated branch reconciliation"
@@ -2839,8 +2876,12 @@ for (let i = 0;i < args.length; i++) {
2839
2876
  repoPath = arg;
2840
2877
  }
2841
2878
  if (showHelp) {
2879
+ let version = "?";
2880
+ try {
2881
+ version = JSON.parse(await Bun.file(new URL("./package.json", import.meta.url).pathname).text()).version;
2882
+ } catch {}
2842
2883
  console.log(`
2843
- xab \u2014 AI-powered curated branch reconciliation
2884
+ xab v${version} \u2014 AI-powered curated branch reconciliation
2844
2885
 
2845
2886
  Usage:
2846
2887
  xab [repo-path] [options]
@@ -2865,7 +2906,7 @@ Behavior:
2865
2906
  --no-fetch Skip fetch (default)
2866
2907
  --no-review Skip Claude review pass
2867
2908
  --no-auto-skip Don't auto-skip commits AI identifies as present
2868
- --max-attempts <n> Max retries per commit (default: 2)
2909
+ --max-attempts <n> Max retries per commit (default: unlimited)
2869
2910
  --no-resume Don't resume from interrupted runs (default: auto-resume)
2870
2911
  --config <path> Path to config file (default: auto-discover)
2871
2912
  --help, -h Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "5.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@
32
32
  "ink-spinner": "^5.0.0",
33
33
  "ink-text-input": "^6.0.0",
34
34
  "react": "18.3.1",
35
- "simple-git": "^3.33.0"
35
+ "simple-git": "^3.33.0",
36
+ "xab": "^5.0.0"
36
37
  }
37
38
  }