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.
- package/dist/index.js +85 -44
- 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:
|
|
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:
|
|
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
|
|
1177
|
-
const modified = status.modified.filter((f) => !
|
|
1178
|
-
const created = status.created.filter((f) => !
|
|
1179
|
-
const deleted = status.deleted.filter((f) => !
|
|
1180
|
-
const notAdded = status.not_added.filter((f) => !
|
|
1181
|
-
const conflicted = status.conflicted.filter((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
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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 ??
|
|
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}
|
|
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
|
-
|
|
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 =
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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": "
|
|
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
|
}
|