xab 3.0.0 → 4.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 +222 -30
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -784,7 +784,15 @@ You can:
784
784
 
785
785
  You MUST NOT modify the worktree in any way. No file writes, no git commits, no destructive commands.
786
786
 
787
- Run relevant tests if you can determine the test command from the repo. Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
787
+ Testing guidelines:
788
+ - Only run tests that work without API keys, secrets, or external service connections
789
+ - Before running a test, check if it needs env vars by reading the test file or relevant .env.example
790
+ - If a test needs keys, only run it if you can see a .env file with those vars already populated
791
+ - Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
792
+ - Avoid: integration tests hitting external APIs, tests requiring running databases/services
793
+ - If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
794
+
795
+ Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
788
796
  }
789
797
  });
790
798
  let resultText = "";
@@ -1151,17 +1159,23 @@ async function validateApply(worktreeGit, beforeHash) {
1151
1159
  errors.push(`Failed to check commits: ${e.message}`);
1152
1160
  }
1153
1161
  const status = await worktreeGit.status();
1154
- const worktreeClean = status.modified.length === 0 && status.created.length === 0 && status.deleted.length === 0 && status.conflicted.length === 0 && status.not_added.length === 0;
1162
+ const isOurs = (f) => f.startsWith(".backmerge/") || f.startsWith(".backmerge\\");
1163
+ const modified = status.modified.filter((f) => !isOurs(f));
1164
+ const created = status.created.filter((f) => !isOurs(f));
1165
+ const deleted = status.deleted.filter((f) => !isOurs(f));
1166
+ const notAdded = status.not_added.filter((f) => !isOurs(f));
1167
+ const conflicted = status.conflicted.filter((f) => !isOurs(f));
1168
+ const worktreeClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && conflicted.length === 0 && notAdded.length === 0;
1155
1169
  if (!worktreeClean) {
1156
1170
  const parts = [];
1157
- if (status.modified.length)
1158
- parts.push(`${status.modified.length} modified`);
1159
- if (status.not_added.length)
1160
- parts.push(`${status.not_added.length} untracked`);
1161
- if (status.deleted.length)
1162
- parts.push(`${status.deleted.length} deleted`);
1163
- if (status.conflicted.length)
1164
- parts.push(`${status.conflicted.length} conflicted`);
1171
+ if (modified.length)
1172
+ parts.push(`${modified.length} modified`);
1173
+ if (notAdded.length)
1174
+ parts.push(`${notAdded.length} untracked`);
1175
+ if (deleted.length)
1176
+ parts.push(`${deleted.length} deleted`);
1177
+ if (conflicted.length)
1178
+ parts.push(`${conflicted.length} conflicted`);
1165
1179
  errors.push(`Working tree not clean: ${parts.join(", ")}`);
1166
1180
  }
1167
1181
  const conflictMarkers = [];
@@ -1192,7 +1206,7 @@ async function getAppliedDiffStat(worktreeGit, beforeHash) {
1192
1206
  }
1193
1207
  async function resetHard(git, ref) {
1194
1208
  await git.raw(["reset", "--hard", ref]);
1195
- await git.raw(["clean", "-fd"]);
1209
+ await git.raw(["clean", "-fd", "--exclude=.backmerge"]);
1196
1210
  }
1197
1211
  async function fetchOrigin(git) {
1198
1212
  await git.fetch(["--all", "--prune"]);
@@ -1335,7 +1349,7 @@ async function runEngine(opts, cb) {
1335
1349
  cb.onLog(`Eval worktree (detached): ${wtPath}`, "green");
1336
1350
  const wtGit = createGit(wtPath);
1337
1351
  const runId = `run-${ts}`;
1338
- const audit = new AuditLog(wtPath, runId);
1352
+ const audit = new AuditLog(repoPath, runId);
1339
1353
  const runMeta = {
1340
1354
  runId,
1341
1355
  startedAt: new Date().toISOString(),
@@ -1736,34 +1750,200 @@ var exports_batch = {};
1736
1750
  __export(exports_batch, {
1737
1751
  runBatch: () => runBatch
1738
1752
  });
1739
- function emit(obj) {
1753
+ import chalk from "chalk";
1754
+ function shortHash2(h) {
1755
+ return h.slice(0, 8);
1756
+ }
1757
+ function ts() {
1758
+ return chalk.dim(new Date().toISOString().slice(11, 19));
1759
+ }
1760
+ function progressBar2(current, total, width = 25) {
1761
+ const ratio = Math.min(current / total, 1);
1762
+ const filled = Math.round(ratio * width);
1763
+ const empty = width - filled;
1764
+ return `${chalk.green("\u2588".repeat(filled))}${chalk.dim("\u2591".repeat(empty))} ${current}/${total}`;
1765
+ }
1766
+ function divider(char = "\u2500", width = 55) {
1767
+ return chalk.dim(char.repeat(width));
1768
+ }
1769
+ function colorize(color, text) {
1770
+ if (!color)
1771
+ return text;
1772
+ return (colorMap[color] ?? chalk.white)(text);
1773
+ }
1774
+ function decisionBadge(kind) {
1775
+ switch (kind) {
1776
+ case "applied":
1777
+ return chalk.bgGreen.black.bold(" APPLIED ");
1778
+ case "would_apply":
1779
+ return chalk.bgCyan.black.bold(" WOULD APPLY ");
1780
+ case "already_applied":
1781
+ return chalk.bgBlue.white.bold(" ALREADY APPLIED ");
1782
+ case "skip":
1783
+ return chalk.bgYellow.black.bold(" SKIP ");
1784
+ case "failed":
1785
+ return chalk.bgRed.white.bold(" FAILED ");
1786
+ }
1787
+ }
1788
+ function analysisBadge(status) {
1789
+ switch (status) {
1790
+ case "yes":
1791
+ return chalk.green.bold("PRESENT");
1792
+ case "no":
1793
+ return chalk.red.bold("MISSING");
1794
+ case "partial":
1795
+ return chalk.yellow.bold("PARTIAL");
1796
+ }
1797
+ }
1798
+ function log(msg) {
1799
+ process.stderr.write(msg + `
1800
+ `);
1801
+ }
1802
+ function emitJsonl(obj) {
1740
1803
  process.stdout.write(JSON.stringify({ ...obj, ts: new Date().toISOString() }) + `
1741
1804
  `);
1742
1805
  }
1743
1806
  async function runBatch(opts) {
1807
+ const jsonl = opts.jsonl ?? false;
1808
+ const startTime = Date.now();
1809
+ log("");
1810
+ log(` ${chalk.cyan.bold("xab")} ${chalk.dim("\u2014 curated branch reconciliation")}`);
1811
+ log(` ${chalk.magenta(opts.sourceRef)} ${chalk.dim("\u2192")} ${chalk.green(opts.targetRef)}`);
1812
+ if (opts.workBranch)
1813
+ log(` ${chalk.dim("work branch:")} ${chalk.cyan(opts.workBranch)}`);
1814
+ if (opts.dryRun)
1815
+ log(` ${chalk.yellow.bold("DRY RUN")}`);
1816
+ log(` ${divider()}`);
1817
+ log("");
1744
1818
  const cb = {
1745
1819
  onLog(msg, color) {
1746
- emit({ event: "log", msg, color });
1820
+ if (jsonl)
1821
+ emitJsonl({ event: "log", msg, color });
1822
+ if (!msg) {
1823
+ log("");
1824
+ return;
1825
+ }
1826
+ log(` ${ts()} ${colorize(color, msg)}`);
1747
1827
  },
1748
1828
  onStatus(msg) {
1749
- emit({ event: "status", msg });
1829
+ if (jsonl)
1830
+ emitJsonl({ event: "status", msg });
1831
+ log(` ${ts()} ${chalk.dim("\u203A")} ${msg}`);
1750
1832
  },
1751
1833
  onCommitStart(commit, index, total) {
1752
- emit({ event: "commit_start", hash: commit.hash, message: commit.message, index, total });
1834
+ if (jsonl)
1835
+ emitJsonl({ event: "commit_start", hash: commit.hash, message: commit.message, index, total });
1836
+ log("");
1837
+ log(` ${divider()}`);
1838
+ log(` ${progressBar2(index + 1, total)} ${chalk.yellow(shortHash2(commit.hash))}`);
1839
+ log(` ${chalk.bold(commit.message)}`);
1840
+ log(` ${chalk.dim(`by ${commit.author} \xB7 ${commit.date}`)}`);
1841
+ log("");
1753
1842
  },
1754
1843
  onAnalysis(commit, analysis) {
1755
- emit({ event: "analysis", hash: commit.hash, result: analysis });
1844
+ if (jsonl)
1845
+ emitJsonl({ event: "analysis", hash: commit.hash, result: analysis });
1846
+ log(` ${ts()} ${chalk.dim("analysis:")} ${analysisBadge(analysis.alreadyInTarget)}`);
1847
+ log(` ${chalk.dim(" summary:")} ${analysis.summary.slice(0, 120)}`);
1848
+ if (analysis.affectedComponents.length > 0) {
1849
+ log(` ${chalk.dim(" components:")} ${analysis.affectedComponents.join(", ")}`);
1850
+ }
1851
+ if (analysis.opsNotes.length > 0) {
1852
+ log(` ${chalk.yellow(" ops:")} ${analysis.opsNotes.join("; ")}`);
1853
+ }
1756
1854
  },
1757
1855
  onDecision(commit, decision) {
1758
- emit({ event: "decision", hash: commit.hash, kind: decision.kind, reason: decision.reason });
1856
+ if (jsonl)
1857
+ emitJsonl({
1858
+ event: "decision",
1859
+ hash: commit.hash,
1860
+ kind: decision.kind,
1861
+ reason: decision.reason,
1862
+ opsNotes: decision.opsNotes
1863
+ });
1864
+ const duration = chalk.dim(`${(decision.durationMs / 1000).toFixed(1)}s`);
1865
+ log(` ${ts()} ${decisionBadge(decision.kind)} ${duration}`);
1866
+ if (decision.kind === "failed" && decision.error) {
1867
+ log(` ${chalk.red(` error: ${decision.error.slice(0, 150)}`)}`);
1868
+ }
1869
+ if (decision.filesChanged && decision.filesChanged.length > 0) {
1870
+ const shown = decision.filesChanged.slice(0, 5).join(", ");
1871
+ const extra = decision.filesChanged.length > 5 ? chalk.dim(` +${decision.filesChanged.length - 5} more`) : "";
1872
+ log(` ${chalk.dim(` files: ${shown}`)}${extra}`);
1873
+ }
1759
1874
  },
1760
1875
  onReview(commit, review) {
1761
- emit({ event: "review", hash: commit.hash, approved: review.approved, issues: review.issues });
1876
+ if (jsonl)
1877
+ emitJsonl({ event: "review", hash: commit.hash, approved: review.approved, issues: review.issues });
1878
+ const badge = review.approved ? chalk.bgGreen.black.bold(" REVIEW OK ") : chalk.bgRed.white.bold(" REVIEW REJECTED ");
1879
+ log(` ${ts()} ${badge} ${chalk.dim(`confidence: ${review.confidence}`)}`);
1880
+ if (!review.approved && review.issues.length > 0) {
1881
+ for (const issue of review.issues.slice(0, 3)) {
1882
+ log(` ${chalk.red(` \xB7 ${issue.slice(0, 120)}`)}`);
1883
+ }
1884
+ if (review.issues.length > 3) {
1885
+ log(` ${chalk.dim(` ...and ${review.issues.length - 3} more issues`)}`);
1886
+ }
1887
+ }
1762
1888
  }
1763
1889
  };
1890
+ let result;
1764
1891
  try {
1765
- const result = await runEngine(opts, cb);
1766
- emit({
1892
+ result = await runEngine(opts, cb);
1893
+ } catch (e) {
1894
+ if (jsonl)
1895
+ emitJsonl({ event: "fatal", error: e.message });
1896
+ log("");
1897
+ log(` ${chalk.bgRed.white.bold(" FATAL ")} ${e.message}`);
1898
+ log("");
1899
+ return 1;
1900
+ }
1901
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
1902
+ const { summary } = result;
1903
+ log("");
1904
+ log(` ${divider("\u2550")}`);
1905
+ log(` ${chalk.bold("Run complete")} ${chalk.dim(`in ${elapsed}s`)}`);
1906
+ log("");
1907
+ const counters = [
1908
+ summary.applied > 0 && `${chalk.green.bold(`${summary.applied}`)} applied`,
1909
+ summary.wouldApply > 0 && `${chalk.cyan.bold(`${summary.wouldApply}`)} would apply`,
1910
+ summary.alreadyApplied > 0 && `${chalk.blue.bold(`${summary.alreadyApplied}`)} already applied`,
1911
+ summary.skipped > 0 && `${chalk.yellow.bold(`${summary.skipped}`)} skipped`,
1912
+ summary.cherrySkipped > 0 && `${chalk.cyan(`${summary.cherrySkipped}`)} cherry-skipped`,
1913
+ summary.failed > 0 && `${chalk.red.bold(`${summary.failed}`)} failed`
1914
+ ].filter(Boolean);
1915
+ log(` ${counters.join(chalk.dim(" \xB7 "))}`);
1916
+ if (result.worktreePath)
1917
+ log(` ${chalk.dim("worktree:")} ${result.worktreePath}`);
1918
+ if (result.workBranch)
1919
+ log(` ${chalk.dim("branch:")} ${result.workBranch}`);
1920
+ if (result.auditDir)
1921
+ log(` ${chalk.dim("audit:")} ${result.auditDir}`);
1922
+ if (result.opsNotes.length > 0) {
1923
+ log("");
1924
+ log(` ${chalk.yellow.bold("OPERATOR NOTES")}`);
1925
+ log(` ${chalk.yellow(divider("\u2500", 40))}`);
1926
+ for (const entry of result.opsNotes) {
1927
+ log(` ${chalk.yellow(shortHash2(entry.commitHash))} ${entry.commitMessage}`);
1928
+ for (const note of entry.notes) {
1929
+ log(` ${chalk.yellow("\u2192")} ${note}`);
1930
+ }
1931
+ }
1932
+ }
1933
+ const failed = result.decisions.filter((d) => d.kind === "failed");
1934
+ if (failed.length > 0) {
1935
+ log("");
1936
+ log(` ${chalk.red.bold("FAILED COMMITS")}`);
1937
+ log(` ${chalk.red(divider("\u2500", 40))}`);
1938
+ for (const d of failed) {
1939
+ log(` ${chalk.red(shortHash2(d.commitHash))} ${d.commitMessage}`);
1940
+ log(` ${chalk.dim("phase:")} ${d.failedPhase ?? "?"} ${chalk.dim("error:")} ${d.error?.slice(0, 100) ?? "?"}`);
1941
+ }
1942
+ }
1943
+ log(` ${divider("\u2550")}`);
1944
+ log("");
1945
+ if (jsonl) {
1946
+ emitJsonl({
1767
1947
  event: "done",
1768
1948
  summary: result.summary,
1769
1949
  worktree: result.worktreePath,
@@ -1771,17 +1951,24 @@ async function runBatch(opts) {
1771
1951
  auditDir: result.auditDir,
1772
1952
  opsNotes: result.opsNotes
1773
1953
  });
1774
- const { summary } = result;
1775
- if (summary.failed > 0)
1776
- return 2;
1777
- return 0;
1778
- } catch (e) {
1779
- emit({ event: "fatal", error: e.message });
1780
- return 1;
1781
1954
  }
1955
+ if (summary.failed > 0)
1956
+ return 2;
1957
+ return 0;
1782
1958
  }
1959
+ var colorMap;
1783
1960
  var init_batch = __esm(() => {
1784
1961
  init_engine();
1962
+ colorMap = {
1963
+ red: chalk.red,
1964
+ green: chalk.green,
1965
+ yellow: chalk.yellow,
1966
+ blue: chalk.blue,
1967
+ magenta: chalk.magenta,
1968
+ cyan: chalk.cyan,
1969
+ gray: chalk.gray,
1970
+ white: chalk.white
1971
+ };
1785
1972
  });
1786
1973
 
1787
1974
  // index.ts
@@ -2548,6 +2735,7 @@ var startAfter = "";
2548
2735
  var limit = 0;
2549
2736
  var configPath = "";
2550
2737
  var batch = false;
2738
+ var jsonl = false;
2551
2739
  var resume = true;
2552
2740
  var showHelp = false;
2553
2741
  for (let i = 0;i < args.length; i++) {
@@ -2556,7 +2744,10 @@ for (let i = 0;i < args.length; i++) {
2556
2744
  showHelp = true;
2557
2745
  else if (arg === "--batch" || arg === "-b")
2558
2746
  batch = true;
2559
- else if (arg === "--dry-run")
2747
+ else if (arg === "--jsonl") {
2748
+ batch = true;
2749
+ jsonl = true;
2750
+ } else if (arg === "--dry-run")
2560
2751
  dryRun = true;
2561
2752
  else if (arg === "--list-only")
2562
2753
  listOnly = true;
@@ -2694,7 +2885,8 @@ if (batch || listOnly) {
2694
2885
  repoPath: resolvedPath,
2695
2886
  sourceRef,
2696
2887
  targetRef,
2697
- ...engineOpts
2888
+ ...engineOpts,
2889
+ jsonl
2698
2890
  });
2699
2891
  process.exit(exitCode);
2700
2892
  } else {
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "xab": "dist/index.js"
8
8
  },
9
- "private":false,
9
+ "private": false,
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "bun build index.ts --outdir dist --target bun --format esm --external @anthropic-ai/claude-agent-sdk --external @openai/codex-sdk --external simple-git --external ink --external ink-select-input --external ink-spinner --external ink-text-input --external react",
14
+ "build": "bun build index.ts --outdir dist --target bun --format esm --external @anthropic-ai/claude-agent-sdk --external @openai/codex-sdk --external simple-git --external ink --external ink-select-input --external ink-spinner --external ink-text-input --external react --external chalk",
15
15
  "prepublishOnly": "bun run build",
16
16
  "start": "bun run index.ts",
17
17
  "dev": "bun run --watch index.ts"
@@ -26,6 +26,7 @@
26
26
  "dependencies": {
27
27
  "@anthropic-ai/claude-agent-sdk": "^0.2.85",
28
28
  "@openai/codex-sdk": "^0.117.0",
29
+ "chalk": "^5.6.2",
29
30
  "ink": "^5.2.1",
30
31
  "ink-select-input": "^6.2.0",
31
32
  "ink-spinner": "^5.0.0",