xab 6.0.0 → 8.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 +174 -50
  2. package/package.json +1 -1
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
  });
@@ -433,11 +433,65 @@ function parseJson(raw, fallback) {
433
433
  }
434
434
  return fallback;
435
435
  }
436
+ async function runStreamedWithProgress(thread, prompt, onProgress, turnOpts) {
437
+ if (!onProgress) {
438
+ const turn = await thread.run(prompt, turnOpts);
439
+ return turn.finalResponse;
440
+ }
441
+ const { events } = await thread.runStreamed(prompt, turnOpts);
442
+ let finalResponse = "";
443
+ for await (const event of events) {
444
+ if (event.type === "item.started" || event.type === "item.updated" || event.type === "item.completed") {
445
+ const item = event.item;
446
+ switch (item.type) {
447
+ case "command_execution": {
448
+ const cmd = item.command ?? "";
449
+ const status = item.status;
450
+ if (status === "in_progress") {
451
+ onProgress("exec", `$ ${cmd}`);
452
+ } else if (status === "completed") {
453
+ const output = item.aggregated_output ?? "";
454
+ if (output) {
455
+ const lines = output.split(`
456
+ `).filter(Boolean);
457
+ for (const line of lines.slice(-3)) {
458
+ onProgress("exec", ` ${line.slice(0, 120)}`);
459
+ }
460
+ }
461
+ }
462
+ break;
463
+ }
464
+ case "file_change": {
465
+ const changes = item.changes ?? [];
466
+ for (const c of changes) {
467
+ onProgress("file", `${c.kind} ${c.path}`);
468
+ }
469
+ break;
470
+ }
471
+ case "reasoning": {
472
+ const text = item.text ?? "";
473
+ if (text && event.type === "item.completed") {
474
+ onProgress("think", text.split(`
475
+ `)[0].slice(0, 120));
476
+ }
477
+ break;
478
+ }
479
+ case "agent_message": {
480
+ if (event.type === "item.completed") {
481
+ finalResponse = item.text ?? "";
482
+ }
483
+ break;
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return finalResponse;
489
+ }
436
490
  async function analyzeCommit(opts) {
437
491
  const codex = new Codex;
438
492
  const thread = codex.startThread({
439
493
  workingDirectory: opts.worktreePath,
440
- sandboxMode: "read-only",
494
+ sandboxMode: "danger-full-access",
441
495
  model: "gpt-5.4",
442
496
  modelReasoningEffort: "high"
443
497
  });
@@ -481,7 +535,7 @@ You are looking at a worktree based on the TARGET branch "${opts.targetBranch}".
481
535
  - Config files that need manual updates on servers? \u2192 note which
482
536
  - Dependencies on external services being added or removed? \u2192 note what
483
537
  - If the commit is just normal code changes that only need a deploy+restart, leave opsNotes as []`;
484
- let turn;
538
+ let response;
485
539
  if (diffChunks.length > 1) {
486
540
  await thread.run(firstPrompt);
487
541
  for (let i = 1;i < diffChunks.length - 1; i++) {
@@ -493,17 +547,17 @@ ${diffChunks[i]}
493
547
  Continue reading. More parts coming.`);
494
548
  }
495
549
  const lastIdx = diffChunks.length - 1;
496
- turn = await thread.run(`### Diff (part ${lastIdx + 1}/${diffChunks.length} \u2014 final):
550
+ response = await runStreamedWithProgress(thread, `### Diff (part ${lastIdx + 1}/${diffChunks.length} \u2014 final):
497
551
  \`\`\`diff
498
552
  ${diffChunks[lastIdx]}
499
553
  \`\`\`
500
554
 
501
- You now have the complete diff. Analyze and produce your structured response.`, { outputSchema: analysisSchema });
555
+ You now have the complete diff. Analyze and produce your structured response.`, opts.onProgress, { outputSchema: analysisSchema });
502
556
  } else {
503
- turn = await thread.run(firstPrompt, { outputSchema: analysisSchema });
557
+ response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: analysisSchema });
504
558
  }
505
- return parseJson(turn.finalResponse, {
506
- summary: turn.finalResponse.slice(0, 500),
559
+ return parseJson(response, {
560
+ summary: response.slice(0, 500),
507
561
  alreadyInTarget: "no",
508
562
  reasoning: "Could not parse structured output",
509
563
  applicationStrategy: "Manual review recommended",
@@ -515,7 +569,7 @@ async function applyCommit(opts) {
515
569
  const codex = new Codex;
516
570
  const thread = codex.startThread({
517
571
  workingDirectory: opts.worktreePath,
518
- sandboxMode: "workspace-write",
572
+ sandboxMode: "danger-full-access",
519
573
  model: "gpt-5.4",
520
574
  modelReasoningEffort: "high"
521
575
  });
@@ -543,7 +597,9 @@ After making all file changes, you MUST run these two commands as your FINAL act
543
597
  If you do not run both commands, your work will be discarded. This is not optional.
544
598
  The validation system checks for exactly one new git commit. Zero commits = failure.
545
599
 
546
- Report what you did.`;
600
+ Report what you did.
601
+
602
+ MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
547
603
  const firstPrompt = `${MERGE_PREAMBLE}
548
604
  ${opts.repoContext ? `## Repository context
549
605
  ${opts.repoContext}
@@ -558,7 +614,7 @@ ${diffChunks[0]}
558
614
  \`\`\`
559
615
 
560
616
  ${diffChunks.length > 1 ? "I will send the remaining diff parts next. Read them all before applying." : instructions}`;
561
- let turn;
617
+ let response;
562
618
  if (diffChunks.length > 1) {
563
619
  await thread.run(firstPrompt);
564
620
  for (let i = 1;i < diffChunks.length - 1; i++) {
@@ -570,22 +626,22 @@ ${diffChunks[i]}
570
626
  Continue reading. More parts coming.`);
571
627
  }
572
628
  const lastIdx = diffChunks.length - 1;
573
- turn = await thread.run(`### Diff (part ${lastIdx + 1}/${diffChunks.length} \u2014 final):
629
+ response = await runStreamedWithProgress(thread, `### Diff (part ${lastIdx + 1}/${diffChunks.length} \u2014 final):
574
630
  \`\`\`diff
575
631
  ${diffChunks[lastIdx]}
576
632
  \`\`\`
577
633
 
578
634
  You now have the complete diff.
579
635
 
580
- ${instructions}`, { outputSchema: applyResultSchema });
636
+ ${instructions}`, opts.onProgress, { outputSchema: applyResultSchema });
581
637
  } else {
582
- turn = await thread.run(firstPrompt, { outputSchema: applyResultSchema });
638
+ response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: applyResultSchema });
583
639
  }
584
- return parseJson(turn.finalResponse, {
640
+ return parseJson(response, {
585
641
  applied: false,
586
642
  filesChanged: [],
587
643
  commitMessage: commitMsg,
588
- notes: turn.finalResponse.slice(0, 1000),
644
+ notes: response.slice(0, 1000),
589
645
  adaptations: ""
590
646
  });
591
647
  }
@@ -593,7 +649,7 @@ async function fixFromReview(opts) {
593
649
  const codex = new Codex;
594
650
  const thread = codex.startThread({
595
651
  workingDirectory: opts.worktreePath,
596
- sandboxMode: "workspace-write",
652
+ sandboxMode: "danger-full-access",
597
653
  model: "gpt-5.4",
598
654
  modelReasoningEffort: "high"
599
655
  });
@@ -622,7 +678,9 @@ After making all fixes, you MUST run these two commands as your FINAL action:
622
678
 
623
679
  If you do not run both commands, your fixes will be discarded. This is not optional.
624
680
 
625
- Report what you fixed.`;
681
+ Report what you fixed.
682
+
683
+ MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
626
684
  const turn = await thread.run(prompt, { outputSchema: applyResultSchema });
627
685
  return parseJson(turn.finalResponse, {
628
686
  applied: false,
@@ -710,7 +768,7 @@ function writeReviewPacket(audit, packet, attempt) {
710
768
  audit.writeAppliedPatch(commitHash, attempt, packet.appliedDiff);
711
769
  }
712
770
  }
713
- async function reviewAppliedDiff(worktreePath, packet) {
771
+ async function reviewAppliedDiff(worktreePath, packet, onProgress) {
714
772
  const strictnessInstructions = {
715
773
  strict: "Be very strict. Any questionable change should be rejected. Err on the side of caution.",
716
774
  normal: "Be thorough but reasonable. Reject clear issues, accept minor style differences.",
@@ -783,7 +841,7 @@ If you have ANY objections, be specific about what's wrong and how to fix it. Yo
783
841
  options: {
784
842
  cwd: worktreePath,
785
843
  model: "claude-opus-4-6",
786
- maxTurns: 30,
844
+ maxTurns: undefined,
787
845
  permissionMode: "default",
788
846
  outputFormat: reviewSchema,
789
847
  tools: ["Read", "Glob", "Grep", "Bash"],
@@ -796,14 +854,20 @@ You can:
796
854
  - Run tests, linters, type-checkers, and build commands via Bash to verify correctness
797
855
  - Run any read-only shell command (cat, ls, git diff, git log, etc.)
798
856
 
799
- You MUST NOT modify the worktree in any way. No file writes, no git commits, no destructive commands.
857
+ You MUST NOT modify the worktree. Specifically:
858
+ - NO file writes, edits, or creates
859
+ - NO git commit, git add, git reset, or any git mutation
860
+ - NO npm install, bun install, yarn install, pnpm install, or any package manager install
861
+ - NO rm, mv, cp, or any file mutation commands
862
+ - NO pip install, cargo build, go get, or anything that writes to disk
800
863
 
801
864
  Testing guidelines:
865
+ - Only run tests that work without installing dependencies (assume deps are already installed if node_modules exists)
802
866
  - Only run tests that work without API keys, secrets, or external service connections
803
867
  - Before running a test, check if it needs env vars by reading the test file or relevant .env.example
804
- - If a test needs keys, only run it if you can see a .env file with those vars already populated
805
868
  - Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
806
869
  - Avoid: integration tests hitting external APIs, tests requiring running databases/services
870
+ - Do NOT run bun install, npm install, or equivalent \u2014 deps are already there if they exist
807
871
  - If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
808
872
 
809
873
  Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
@@ -811,6 +875,34 @@ Your objections will be sent back to the apply agent for fixing, so be specific
811
875
  });
812
876
  let resultText = "";
813
877
  for await (const message of q) {
878
+ if (onProgress && message.type === "assistant") {
879
+ const betaMsg = message.message;
880
+ const content = betaMsg?.content;
881
+ if (content) {
882
+ for (const block of content) {
883
+ if (block.type === "tool_use") {
884
+ const name = block.name;
885
+ const input = block.input;
886
+ if (name === "Bash") {
887
+ onProgress("review", `$ ${(input.command ?? "").slice(0, 120)}`);
888
+ } else if (name === "Read") {
889
+ onProgress("review", `read ${(input.file_path ?? "").replace(worktreePath + "/", "")}`);
890
+ } else if (name === "Grep") {
891
+ onProgress("review", `grep "${(input.pattern ?? "").slice(0, 60)}"`);
892
+ } else if (name === "Glob") {
893
+ onProgress("review", `glob ${(input.pattern ?? "").slice(0, 60)}`);
894
+ } else {
895
+ onProgress("review", `${name}`);
896
+ }
897
+ } else if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
898
+ const firstLine = block.text.split(`
899
+ `)[0].slice(0, 120);
900
+ if (firstLine)
901
+ onProgress("review", firstLine);
902
+ }
903
+ }
904
+ }
905
+ }
814
906
  if (message.type === "result") {
815
907
  if ("result" in message) {
816
908
  resultText = message.result;
@@ -844,17 +936,22 @@ async function verifyReviewIntegrity(wtGit, expectedHead) {
844
936
  return `Review mutated HEAD: expected ${expectedHead.slice(0, 8)}, got ${currentHead.slice(0, 8)}`;
845
937
  }
846
938
  const status = await wtGit.status();
847
- const dirty = status.modified.length + status.created.length + status.deleted.length + status.not_added.length + status.conflicted.length;
939
+ const isInfra = (f) => f.startsWith(".backmerge/") || f.startsWith(".git-local/") || f.startsWith("node_modules/") || f.startsWith(".cache/") || f.startsWith("dist/") || f.startsWith("build/") || f.startsWith("target/");
940
+ const modified = status.modified.filter((f) => !isInfra(f));
941
+ const created = status.created.filter((f) => !isInfra(f));
942
+ const deleted = status.deleted.filter((f) => !isInfra(f));
943
+ const notAdded = status.not_added.filter((f) => !isInfra(f));
944
+ const dirty = modified.length + created.length + deleted.length + notAdded.length + status.conflicted.length;
848
945
  if (dirty > 0) {
849
946
  const parts = [];
850
- if (status.modified.length)
851
- parts.push(`${status.modified.length} modified`);
852
- if (status.created.length)
853
- parts.push(`${status.created.length} staged`);
854
- if (status.not_added.length)
855
- parts.push(`${status.not_added.length} untracked`);
856
- if (status.deleted.length)
857
- parts.push(`${status.deleted.length} deleted`);
947
+ if (modified.length)
948
+ parts.push(`${modified.length} modified`);
949
+ if (notAdded.length)
950
+ parts.push(`${notAdded.length} untracked`);
951
+ if (deleted.length)
952
+ parts.push(`${deleted.length} deleted`);
953
+ if (status.conflicted.length)
954
+ parts.push(`${status.conflicted.length} conflicted`);
858
955
  return `Review left dirty worktree: ${parts.join(", ")}`;
859
956
  }
860
957
  return null;
@@ -1173,12 +1270,12 @@ async function validateApply(worktreeGit, beforeHash) {
1173
1270
  errors.push(`Failed to check commits: ${e.message}`);
1174
1271
  }
1175
1272
  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));
1273
+ const isInfra = (f) => f.startsWith(".backmerge/") || f.startsWith(".backmerge\\") || f.startsWith(".git-local/") || f.startsWith(".git-local\\");
1274
+ const modified = status.modified.filter((f) => !isInfra(f));
1275
+ const created = status.created.filter((f) => !isInfra(f));
1276
+ const deleted = status.deleted.filter((f) => !isInfra(f));
1277
+ const notAdded = status.not_added.filter((f) => !isInfra(f));
1278
+ const conflicted = status.conflicted.filter((f) => !isInfra(f));
1182
1279
  const worktreeClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && conflicted.length === 0 && notAdded.length === 0;
1183
1280
  const dirtyFiles = [];
1184
1281
  if (!worktreeClean) {
@@ -1228,7 +1325,7 @@ async function getAppliedDiffStat(worktreeGit, beforeHash) {
1228
1325
  }
1229
1326
  async function resetHard(git, ref) {
1230
1327
  await git.raw(["reset", "--hard", ref]);
1231
- await git.raw(["clean", "-fd", "--exclude=.backmerge"]);
1328
+ await git.raw(["clean", "-fd", "--exclude=.backmerge", "--exclude=.git-local"]);
1232
1329
  }
1233
1330
  async function fetchOrigin(git) {
1234
1331
  await git.fetch(["--all", "--prune"]);
@@ -1268,7 +1365,7 @@ async function runEngine(opts, cb) {
1268
1365
  autoSkip = true
1269
1366
  } = opts;
1270
1367
  const config = loadConfig(repoPath, opts.configPath);
1271
- const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ?? 2;
1368
+ const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ?? Infinity;
1272
1369
  const effectiveWorkBranch = opts.workBranch ?? config.workBranch;
1273
1370
  const commitPrefix = config.commitPrefix ?? "backmerge:";
1274
1371
  if (!checkCodexInstalled())
@@ -1497,7 +1594,8 @@ async function processOneCommit(o) {
1497
1594
  sourceBranch: o.sourceRef,
1498
1595
  targetBranch: o.targetRef,
1499
1596
  sourceLatestDiff: o.sourceLatestDiff,
1500
- repoContext: commitCtx.promptBlock
1597
+ repoContext: commitCtx.promptBlock,
1598
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("analyze", `[${phase}] ${msg}`) : undefined
1501
1599
  });
1502
1600
  audit.writeAnalysis(commit.hash, 1, analysis);
1503
1601
  cb.onAnalysis(commit, analysis);
@@ -1548,7 +1646,7 @@ async function processOneCommit(o) {
1548
1646
  }
1549
1647
  for (let attempt = 1;attempt <= o.maxAttempts; attempt++) {
1550
1648
  const headBefore = await getHead(o.wtGit);
1551
- cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt}/${o.maxAttempts})...`);
1649
+ cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt})...`);
1552
1650
  let applyResult;
1553
1651
  try {
1554
1652
  applyResult = await applyCommit({
@@ -1560,7 +1658,8 @@ async function processOneCommit(o) {
1560
1658
  sourceBranch: o.sourceRef,
1561
1659
  targetBranch: o.targetRef,
1562
1660
  repoContext: commitCtx.promptBlock,
1563
- commitPrefix: o.commitPrefix
1661
+ commitPrefix: o.commitPrefix,
1662
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("apply", `[${phase}] ${msg}`) : undefined
1564
1663
  });
1565
1664
  audit.writeAnalysis(commit.hash, attempt, { ...analysis, applyResult });
1566
1665
  } catch (e) {
@@ -1571,7 +1670,26 @@ async function processOneCommit(o) {
1571
1670
  continue;
1572
1671
  }
1573
1672
  cb.onStatus(`Validating ${commit.hash.slice(0, 8)}...`);
1574
- const validation = await validateApply(o.wtGit, headBefore);
1673
+ let validation = await validateApply(o.wtGit, headBefore);
1674
+ if (!validation.valid && validation.newCommitCount === 0 && !validation.worktreeClean && validation.dirtyFiles.length > 0) {
1675
+ const realChanges = validation.dirtyFiles.filter((f) => !f.includes(".git-local/"));
1676
+ if (realChanges.length > 0) {
1677
+ cb.onLog(`Codex left ${realChanges.length} changed files without committing \u2014 creating rescue commit`, "yellow");
1678
+ for (const f of realChanges)
1679
+ cb.onLog(` ${f}`, "yellow");
1680
+ const commitMsg = `${o.commitPrefix} ${commit.message} (from ${commit.hash.slice(0, 8)})`;
1681
+ try {
1682
+ await o.wtGit.raw(["add", "-A", "--", ".", ":!.git-local"]);
1683
+ await o.wtGit.raw(["commit", "-m", commitMsg]);
1684
+ validation = await validateApply(o.wtGit, headBefore);
1685
+ if (validation.valid) {
1686
+ cb.onLog(`Rescue commit succeeded`, "green");
1687
+ }
1688
+ } catch (e) {
1689
+ cb.onLog(`Rescue commit failed: ${e.message}`, "red");
1690
+ }
1691
+ }
1692
+ }
1575
1693
  if (!validation.valid) {
1576
1694
  cb.onLog(`Validation failed: ${validation.errors.join("; ")}`, "red");
1577
1695
  if (validation.dirtyFiles.length > 0) {
@@ -1607,7 +1725,7 @@ async function processOneCommit(o) {
1607
1725
  };
1608
1726
  writeReviewPacket(audit, packet, attempt);
1609
1727
  const headBeforeReview = await getHead(o.wtGit);
1610
- reviewResult = await reviewAppliedDiff(o.wtPath, packet);
1728
+ reviewResult = await reviewAppliedDiff(o.wtPath, packet, o.cb.onProgress);
1611
1729
  audit.writeReviewResult(commit.hash, attempt, reviewResult);
1612
1730
  const mutation = await verifyReviewIntegrity(o.wtGit, headBeforeReview);
1613
1731
  if (mutation) {
@@ -1625,10 +1743,10 @@ async function processOneCommit(o) {
1625
1743
  }
1626
1744
  if (!reviewResult.approved) {
1627
1745
  cb.onLog(`Review rejected: ${reviewResult.issues.join("; ")}`, "red");
1628
- const maxFixRounds = 2;
1746
+ const maxFixRounds = Infinity;
1629
1747
  let fixed = false;
1630
1748
  for (let fixRound = 1;fixRound <= maxFixRounds; fixRound++) {
1631
- cb.onStatus(`Codex fixing review issues (round ${fixRound}/${maxFixRounds})...`);
1749
+ cb.onStatus(`Codex fixing review issues (round ${fixRound})...`);
1632
1750
  try {
1633
1751
  await fixFromReview({
1634
1752
  worktreePath: o.wtPath,
@@ -1659,7 +1777,7 @@ async function processOneCommit(o) {
1659
1777
  appliedDiffStat: fixedStat
1660
1778
  };
1661
1779
  const headBeforeReReview = await getHead(o.wtGit);
1662
- reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket);
1780
+ reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket, o.cb.onProgress);
1663
1781
  audit.writeReviewResult(commit.hash, attempt, {
1664
1782
  ...reviewResult,
1665
1783
  fixRound
@@ -1690,7 +1808,7 @@ async function processOneCommit(o) {
1690
1808
  kind: "failed",
1691
1809
  commitHash: commit.hash,
1692
1810
  commitMessage: commit.message,
1693
- reason: `Review rejected after ${maxFixRounds} fix rounds: ${reviewResult.issues.join("; ")}`,
1811
+ reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
1694
1812
  failedPhase: "review",
1695
1813
  reviewApproved: false,
1696
1814
  reviewIssues: reviewResult.issues,
@@ -1704,7 +1822,7 @@ async function processOneCommit(o) {
1704
1822
  kind: "failed",
1705
1823
  commitHash: commit.hash,
1706
1824
  commitMessage: commit.message,
1707
- reason: `Review rejected after ${maxFixRounds} fix rounds: ${reviewResult.issues.join("; ")}`,
1825
+ reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
1708
1826
  failedPhase: "review",
1709
1827
  reviewApproved: false,
1710
1828
  reviewIssues: reviewResult.issues,
@@ -1854,6 +1972,12 @@ async function runBatch(opts) {
1854
1972
  log(` ${divider()}`);
1855
1973
  log("");
1856
1974
  const cb = {
1975
+ onProgress(phase, msg) {
1976
+ if (jsonl)
1977
+ emitJsonl({ event: "progress", phase, msg });
1978
+ const icon = phase === "apply" ? chalk.green("\u25B8") : chalk.blue("\u25B8");
1979
+ log(` ${ts()} ${icon} ${chalk.dim(msg)}`);
1980
+ },
1857
1981
  onLog(msg, color) {
1858
1982
  if (jsonl)
1859
1983
  emitJsonl({ event: "log", msg, color });
@@ -2883,7 +3007,7 @@ Behavior:
2883
3007
  --no-fetch Skip fetch (default)
2884
3008
  --no-review Skip Claude review pass
2885
3009
  --no-auto-skip Don't auto-skip commits AI identifies as present
2886
- --max-attempts <n> Max retries per commit (default: 2)
3010
+ --max-attempts <n> Max retries per commit (default: unlimited)
2887
3011
  --no-resume Don't resume from interrupted runs (default: auto-resume)
2888
3012
  --config <path> Path to config file (default: auto-discover)
2889
3013
  --help, -h Show this help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "6.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {