xab 3.0.0 → 5.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 +283 -34
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -531,11 +531,18 @@ ${opts.applicationStrategy}
531
531
  - If the target already has a different version of the same logic, merge both intents
532
532
  - Preserve the target's existing improvements \u2014 do not regress
533
533
  - Create or modify files as needed; delete files if the source commit deleted them
534
- - After applying, run exactly: git add -A && git commit -m "${commitMsg.replace(/"/g, "\\\"")}"
535
- - You MUST create exactly ONE commit. Not zero, not two.
536
534
  - No conflict markers, dead code, or TODO placeholders
537
535
  - If impossible to apply cleanly, explain why in notes
538
536
 
537
+ ## CRITICAL \u2014 you MUST commit your changes
538
+ After making all file changes, you MUST run these two commands as your FINAL action:
539
+
540
+ git add -A
541
+ git commit -m "${commitMsg.replace(/"/g, "\\\"")}"
542
+
543
+ If you do not run both commands, your work will be discarded. This is not optional.
544
+ The validation system checks for exactly one new git commit. Zero commits = failure.
545
+
539
546
  Report what you did.`;
540
547
  const firstPrompt = `${MERGE_PREAMBLE}
541
548
  ${opts.repoContext ? `## Repository context
@@ -605,9 +612,16 @@ ${opts.reviewIssues.map((issue, i) => `${i + 1}. ${issue}`).join(`
605
612
  - Read the affected files to understand the current state
606
613
  - Fix every issue the reviewer raised
607
614
  - Do NOT introduce new problems while fixing
608
- - After fixing, amend the commit: git add -A && git commit --amend -m "${commitMsg.replace(/"/g, "\\\"")}"
609
615
  - The result must be a single clean commit with no issues
610
616
 
617
+ ## CRITICAL \u2014 you MUST amend the commit after fixing
618
+ After making all fixes, you MUST run these two commands as your FINAL action:
619
+
620
+ git add -A
621
+ git commit --amend -m "${commitMsg.replace(/"/g, "\\\"")}"
622
+
623
+ If you do not run both commands, your fixes will be discarded. This is not optional.
624
+
611
625
  Report what you fixed.`;
612
626
  const turn = await thread.run(prompt, { outputSchema: applyResultSchema });
613
627
  return parseJson(turn.finalResponse, {
@@ -784,7 +798,15 @@ You can:
784
798
 
785
799
  You MUST NOT modify the worktree in any way. No file writes, no git commits, no destructive commands.
786
800
 
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.`
801
+ Testing guidelines:
802
+ - Only run tests that work without API keys, secrets, or external service connections
803
+ - 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
+ - Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
806
+ - Avoid: integration tests hitting external APIs, tests requiring running databases/services
807
+ - If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
808
+
809
+ Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
788
810
  }
789
811
  });
790
812
  let resultText = "";
@@ -1151,17 +1173,36 @@ async function validateApply(worktreeGit, beforeHash) {
1151
1173
  errors.push(`Failed to check commits: ${e.message}`);
1152
1174
  }
1153
1175
  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;
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));
1182
+ const worktreeClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && conflicted.length === 0 && notAdded.length === 0;
1183
+ const dirtyFiles = [];
1155
1184
  if (!worktreeClean) {
1156
1185
  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`);
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
+ }
1165
1206
  errors.push(`Working tree not clean: ${parts.join(", ")}`);
1166
1207
  }
1167
1208
  const conflictMarkers = [];
@@ -1182,7 +1223,15 @@ async function validateApply(worktreeGit, beforeHash) {
1182
1223
  if (conflictMarkers.length > 0) {
1183
1224
  errors.push(`Conflict markers in: ${conflictMarkers.join(", ")}`);
1184
1225
  }
1185
- return { valid: errors.length === 0, newCommitHash, newCommitCount, worktreeClean, conflictMarkers, errors };
1226
+ return {
1227
+ valid: errors.length === 0,
1228
+ newCommitHash,
1229
+ newCommitCount,
1230
+ worktreeClean,
1231
+ conflictMarkers,
1232
+ dirtyFiles,
1233
+ errors
1234
+ };
1186
1235
  }
1187
1236
  async function getAppliedDiff(worktreeGit, beforeHash) {
1188
1237
  return worktreeGit.raw(["diff", beforeHash, "HEAD"]);
@@ -1192,7 +1241,7 @@ async function getAppliedDiffStat(worktreeGit, beforeHash) {
1192
1241
  }
1193
1242
  async function resetHard(git, ref) {
1194
1243
  await git.raw(["reset", "--hard", ref]);
1195
- await git.raw(["clean", "-fd"]);
1244
+ await git.raw(["clean", "-fd", "--exclude=.backmerge"]);
1196
1245
  }
1197
1246
  async function fetchOrigin(git) {
1198
1247
  await git.fetch(["--all", "--prune"]);
@@ -1335,7 +1384,7 @@ async function runEngine(opts, cb) {
1335
1384
  cb.onLog(`Eval worktree (detached): ${wtPath}`, "green");
1336
1385
  const wtGit = createGit(wtPath);
1337
1386
  const runId = `run-${ts}`;
1338
- const audit = new AuditLog(wtPath, runId);
1387
+ const audit = new AuditLog(repoPath, runId);
1339
1388
  const runMeta = {
1340
1389
  runId,
1341
1390
  startedAt: new Date().toISOString(),
@@ -1538,6 +1587,11 @@ async function processOneCommit(o) {
1538
1587
  const validation = await validateApply(o.wtGit, headBefore);
1539
1588
  if (!validation.valid) {
1540
1589
  cb.onLog(`Validation failed: ${validation.errors.join("; ")}`, "red");
1590
+ if (validation.dirtyFiles.length > 0) {
1591
+ for (const f of validation.dirtyFiles) {
1592
+ cb.onLog(` ${f}`, "red");
1593
+ }
1594
+ }
1541
1595
  await resetHard(o.wtGit, headBefore);
1542
1596
  if (attempt === o.maxAttempts)
1543
1597
  return mkFailed(commit, "validation", validation.errors.join("; "), start);
@@ -1736,34 +1790,217 @@ var exports_batch = {};
1736
1790
  __export(exports_batch, {
1737
1791
  runBatch: () => runBatch
1738
1792
  });
1739
- function emit(obj) {
1793
+ import chalk from "chalk";
1794
+ function shortHash2(h) {
1795
+ return h.slice(0, 8);
1796
+ }
1797
+ function ts() {
1798
+ return chalk.dim(new Date().toISOString().slice(11, 19));
1799
+ }
1800
+ function progressBar2(current, total, width = 25) {
1801
+ const ratio = Math.min(current / total, 1);
1802
+ const filled = Math.round(ratio * width);
1803
+ const empty = width - filled;
1804
+ return `${chalk.green("\u2588".repeat(filled))}${chalk.dim("\u2591".repeat(empty))} ${current}/${total}`;
1805
+ }
1806
+ function divider(char = "\u2500", width = 55) {
1807
+ return chalk.dim(char.repeat(width));
1808
+ }
1809
+ function colorize(color, text) {
1810
+ if (!color)
1811
+ return text;
1812
+ return (colorMap[color] ?? chalk.white)(text);
1813
+ }
1814
+ function decisionBadge(kind) {
1815
+ switch (kind) {
1816
+ case "applied":
1817
+ return chalk.bgGreen.black.bold(" APPLIED ");
1818
+ case "would_apply":
1819
+ return chalk.bgCyan.black.bold(" WOULD APPLY ");
1820
+ case "already_applied":
1821
+ return chalk.bgBlue.white.bold(" ALREADY APPLIED ");
1822
+ case "skip":
1823
+ return chalk.bgYellow.black.bold(" SKIP ");
1824
+ case "failed":
1825
+ return chalk.bgRed.white.bold(" FAILED ");
1826
+ }
1827
+ }
1828
+ function analysisBadge(status) {
1829
+ switch (status) {
1830
+ case "yes":
1831
+ return chalk.green.bold("PRESENT");
1832
+ case "no":
1833
+ return chalk.red.bold("MISSING");
1834
+ case "partial":
1835
+ return chalk.yellow.bold("PARTIAL");
1836
+ }
1837
+ }
1838
+ function log(msg) {
1839
+ process.stderr.write(msg + `
1840
+ `);
1841
+ }
1842
+ function emitJsonl(obj) {
1740
1843
  process.stdout.write(JSON.stringify({ ...obj, ts: new Date().toISOString() }) + `
1741
1844
  `);
1742
1845
  }
1743
1846
  async function runBatch(opts) {
1847
+ const jsonl = opts.jsonl ?? false;
1848
+ const startTime = Date.now();
1849
+ log("");
1850
+ log(` ${chalk.cyan.bold("xab")} ${chalk.dim("\u2014 curated branch reconciliation")}`);
1851
+ log(` ${chalk.magenta(opts.sourceRef)} ${chalk.dim("\u2192")} ${chalk.green(opts.targetRef)}`);
1852
+ if (opts.workBranch)
1853
+ log(` ${chalk.dim("work branch:")} ${chalk.cyan(opts.workBranch)}`);
1854
+ if (opts.dryRun)
1855
+ log(` ${chalk.yellow.bold("DRY RUN")}`);
1856
+ log(` ${divider()}`);
1857
+ log("");
1744
1858
  const cb = {
1745
1859
  onLog(msg, color) {
1746
- emit({ event: "log", msg, color });
1860
+ if (jsonl)
1861
+ emitJsonl({ event: "log", msg, color });
1862
+ if (!msg) {
1863
+ log("");
1864
+ return;
1865
+ }
1866
+ log(` ${ts()} ${colorize(color, msg)}`);
1747
1867
  },
1748
1868
  onStatus(msg) {
1749
- emit({ event: "status", msg });
1869
+ if (jsonl)
1870
+ emitJsonl({ event: "status", msg });
1871
+ log(` ${ts()} ${chalk.dim("\u203A")} ${msg}`);
1750
1872
  },
1751
1873
  onCommitStart(commit, index, total) {
1752
- emit({ event: "commit_start", hash: commit.hash, message: commit.message, index, total });
1874
+ if (jsonl)
1875
+ emitJsonl({ event: "commit_start", hash: commit.hash, message: commit.message, index, total });
1876
+ log("");
1877
+ log(` ${divider()}`);
1878
+ log(` ${progressBar2(index + 1, total)} ${chalk.yellow(shortHash2(commit.hash))}`);
1879
+ log(` ${chalk.bold(commit.message)}`);
1880
+ log(` ${chalk.dim(`by ${commit.author} \xB7 ${commit.date}`)}`);
1881
+ log("");
1753
1882
  },
1754
1883
  onAnalysis(commit, analysis) {
1755
- emit({ event: "analysis", hash: commit.hash, result: analysis });
1884
+ if (jsonl)
1885
+ emitJsonl({ event: "analysis", hash: commit.hash, result: analysis });
1886
+ log(` ${ts()} ${chalk.dim("analysis:")} ${analysisBadge(analysis.alreadyInTarget)}`);
1887
+ log(` ${chalk.dim(" summary:")} ${analysis.summary}`);
1888
+ if (analysis.reasoning) {
1889
+ log(` ${chalk.dim(" reasoning:")} ${analysis.reasoning}`);
1890
+ }
1891
+ if (analysis.applicationStrategy && analysis.alreadyInTarget !== "yes") {
1892
+ log(` ${chalk.dim(" strategy:")} ${analysis.applicationStrategy}`);
1893
+ }
1894
+ if (analysis.affectedComponents.length > 0) {
1895
+ log(` ${chalk.dim(" components:")} ${analysis.affectedComponents.join(", ")}`);
1896
+ }
1897
+ if (analysis.opsNotes.length > 0) {
1898
+ log(` ${chalk.yellow(" ops:")} ${analysis.opsNotes.join("; ")}`);
1899
+ }
1756
1900
  },
1757
1901
  onDecision(commit, decision) {
1758
- emit({ event: "decision", hash: commit.hash, kind: decision.kind, reason: decision.reason });
1902
+ if (jsonl)
1903
+ emitJsonl({
1904
+ event: "decision",
1905
+ hash: commit.hash,
1906
+ kind: decision.kind,
1907
+ reason: decision.reason,
1908
+ opsNotes: decision.opsNotes
1909
+ });
1910
+ const duration = chalk.dim(`${(decision.durationMs / 1000).toFixed(1)}s`);
1911
+ log(` ${ts()} ${decisionBadge(decision.kind)} ${duration}`);
1912
+ if (decision.newCommitHash) {
1913
+ log(` ${chalk.dim(" commit:")} ${decision.newCommitHash.slice(0, 8)}`);
1914
+ }
1915
+ if (decision.kind === "failed" && decision.error) {
1916
+ log(` ${chalk.red(` error: ${decision.error}`)}`);
1917
+ }
1918
+ if (decision.reason && decision.kind !== "failed") {
1919
+ log(` ${chalk.dim(" reason:")} ${decision.reason}`);
1920
+ }
1921
+ if (decision.filesChanged && decision.filesChanged.length > 0) {
1922
+ for (const f of decision.filesChanged) {
1923
+ log(` ${chalk.dim(` \xB7 ${f}`)}`);
1924
+ }
1925
+ }
1926
+ if (decision.opsNotes && decision.opsNotes.length > 0) {
1927
+ for (const note of decision.opsNotes) {
1928
+ log(` ${chalk.yellow(` ops: ${note}`)}`);
1929
+ }
1930
+ }
1759
1931
  },
1760
1932
  onReview(commit, review) {
1761
- emit({ event: "review", hash: commit.hash, approved: review.approved, issues: review.issues });
1933
+ if (jsonl)
1934
+ emitJsonl({ event: "review", hash: commit.hash, approved: review.approved, issues: review.issues });
1935
+ const badge = review.approved ? chalk.bgGreen.black.bold(" REVIEW OK ") : chalk.bgRed.white.bold(" REVIEW REJECTED ");
1936
+ log(` ${ts()} ${badge} ${chalk.dim(`confidence: ${review.confidence}`)}`);
1937
+ if (!review.approved && review.issues.length > 0) {
1938
+ for (const issue of review.issues.slice(0, 3)) {
1939
+ log(` ${chalk.red(` \xB7 ${issue.slice(0, 120)}`)}`);
1940
+ }
1941
+ if (review.issues.length > 3) {
1942
+ log(` ${chalk.dim(` ...and ${review.issues.length - 3} more issues`)}`);
1943
+ }
1944
+ }
1762
1945
  }
1763
1946
  };
1947
+ let result;
1764
1948
  try {
1765
- const result = await runEngine(opts, cb);
1766
- emit({
1949
+ result = await runEngine(opts, cb);
1950
+ } catch (e) {
1951
+ if (jsonl)
1952
+ emitJsonl({ event: "fatal", error: e.message });
1953
+ log("");
1954
+ log(` ${chalk.bgRed.white.bold(" FATAL ")} ${e.message}`);
1955
+ log("");
1956
+ return 1;
1957
+ }
1958
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
1959
+ const { summary } = result;
1960
+ log("");
1961
+ log(` ${divider("\u2550")}`);
1962
+ log(` ${chalk.bold("Run complete")} ${chalk.dim(`in ${elapsed}s`)}`);
1963
+ log("");
1964
+ const counters = [
1965
+ summary.applied > 0 && `${chalk.green.bold(`${summary.applied}`)} applied`,
1966
+ summary.wouldApply > 0 && `${chalk.cyan.bold(`${summary.wouldApply}`)} would apply`,
1967
+ summary.alreadyApplied > 0 && `${chalk.blue.bold(`${summary.alreadyApplied}`)} already applied`,
1968
+ summary.skipped > 0 && `${chalk.yellow.bold(`${summary.skipped}`)} skipped`,
1969
+ summary.cherrySkipped > 0 && `${chalk.cyan(`${summary.cherrySkipped}`)} cherry-skipped`,
1970
+ summary.failed > 0 && `${chalk.red.bold(`${summary.failed}`)} failed`
1971
+ ].filter(Boolean);
1972
+ log(` ${counters.join(chalk.dim(" \xB7 "))}`);
1973
+ if (result.worktreePath)
1974
+ log(` ${chalk.dim("worktree:")} ${result.worktreePath}`);
1975
+ if (result.workBranch)
1976
+ log(` ${chalk.dim("branch:")} ${result.workBranch}`);
1977
+ if (result.auditDir)
1978
+ log(` ${chalk.dim("audit:")} ${result.auditDir}`);
1979
+ if (result.opsNotes.length > 0) {
1980
+ log("");
1981
+ log(` ${chalk.yellow.bold("OPERATOR NOTES")}`);
1982
+ log(` ${chalk.yellow(divider("\u2500", 40))}`);
1983
+ for (const entry of result.opsNotes) {
1984
+ log(` ${chalk.yellow(shortHash2(entry.commitHash))} ${entry.commitMessage}`);
1985
+ for (const note of entry.notes) {
1986
+ log(` ${chalk.yellow("\u2192")} ${note}`);
1987
+ }
1988
+ }
1989
+ }
1990
+ const failed = result.decisions.filter((d) => d.kind === "failed");
1991
+ if (failed.length > 0) {
1992
+ log("");
1993
+ log(` ${chalk.red.bold("FAILED COMMITS")}`);
1994
+ log(` ${chalk.red(divider("\u2500", 40))}`);
1995
+ for (const d of failed) {
1996
+ log(` ${chalk.red(shortHash2(d.commitHash))} ${d.commitMessage}`);
1997
+ log(` ${chalk.dim("phase:")} ${d.failedPhase ?? "?"} ${chalk.dim("error:")} ${d.error?.slice(0, 100) ?? "?"}`);
1998
+ }
1999
+ }
2000
+ log(` ${divider("\u2550")}`);
2001
+ log("");
2002
+ if (jsonl) {
2003
+ emitJsonl({
1767
2004
  event: "done",
1768
2005
  summary: result.summary,
1769
2006
  worktree: result.worktreePath,
@@ -1771,17 +2008,24 @@ async function runBatch(opts) {
1771
2008
  auditDir: result.auditDir,
1772
2009
  opsNotes: result.opsNotes
1773
2010
  });
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
2011
  }
2012
+ if (summary.failed > 0)
2013
+ return 2;
2014
+ return 0;
1782
2015
  }
2016
+ var colorMap;
1783
2017
  var init_batch = __esm(() => {
1784
2018
  init_engine();
2019
+ colorMap = {
2020
+ red: chalk.red,
2021
+ green: chalk.green,
2022
+ yellow: chalk.yellow,
2023
+ blue: chalk.blue,
2024
+ magenta: chalk.magenta,
2025
+ cyan: chalk.cyan,
2026
+ gray: chalk.gray,
2027
+ white: chalk.white
2028
+ };
1785
2029
  });
1786
2030
 
1787
2031
  // index.ts
@@ -2548,6 +2792,7 @@ var startAfter = "";
2548
2792
  var limit = 0;
2549
2793
  var configPath = "";
2550
2794
  var batch = false;
2795
+ var jsonl = false;
2551
2796
  var resume = true;
2552
2797
  var showHelp = false;
2553
2798
  for (let i = 0;i < args.length; i++) {
@@ -2556,7 +2801,10 @@ for (let i = 0;i < args.length; i++) {
2556
2801
  showHelp = true;
2557
2802
  else if (arg === "--batch" || arg === "-b")
2558
2803
  batch = true;
2559
- else if (arg === "--dry-run")
2804
+ else if (arg === "--jsonl") {
2805
+ batch = true;
2806
+ jsonl = true;
2807
+ } else if (arg === "--dry-run")
2560
2808
  dryRun = true;
2561
2809
  else if (arg === "--list-only")
2562
2810
  listOnly = true;
@@ -2694,7 +2942,8 @@ if (batch || listOnly) {
2694
2942
  repoPath: resolvedPath,
2695
2943
  sourceRef,
2696
2944
  targetRef,
2697
- ...engineOpts
2945
+ ...engineOpts,
2946
+ jsonl
2698
2947
  });
2699
2948
  process.exit(exitCode);
2700
2949
  } else {
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "3.0.0",
3
+ "version": "5.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",