xab 12.0.0 → 13.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 +192 -13
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -201,7 +201,7 @@ function buildRepoContext(repoPath, config) {
201
201
  const docPaths = discoverDocPaths(repoPath);
202
202
  return { structure, instructions, docPaths };
203
203
  }
204
- function buildCommitContext(repoPath, repoCtx, config, touchedPaths, commitMessage) {
204
+ function buildCommitContext(repoPath, repoCtx, config, touchedPaths, commitMessage, memoryBlock) {
205
205
  const includedFiles = [];
206
206
  const sections = [];
207
207
  const rs = repoCtx.structure;
@@ -214,6 +214,9 @@ function buildCommitContext(repoPath, repoCtx, config, touchedPaths, commitMessa
214
214
  structLines.push(`Packages: ${rs.packages.join(", ")}`);
215
215
  sections.push(structLines.join(`
216
216
  `));
217
+ if (memoryBlock) {
218
+ sections.push(memoryBlock);
219
+ }
217
220
  for (const [name, content] of repoCtx.instructions) {
218
221
  sections.push(`--- ${name} ---
219
222
  ${content}`);
@@ -585,7 +588,18 @@ You are looking at a worktree based on the TARGET branch "${opts.targetBranch}".
585
588
  - New services, containers, or infrastructure to deploy? \u2192 note what
586
589
  - Config files that need manual updates on servers? \u2192 note which
587
590
  - Dependencies on external services being added or removed? \u2192 note what
588
- - If the commit is just normal code changes that only need a deploy+restart, leave opsNotes as []`;
591
+ - If the commit is just normal code changes that only need a deploy+restart, leave opsNotes as []
592
+ 8. Record any non-obvious discoveries that would help analyze FUTURE commits:
593
+ - Path mappings between source and target branches (e.g. "frontend/ in source = apps/frontend/ in target")
594
+ - Key functions, helpers, or patterns you found (e.g. "betExitValueLocal() is the shared P&L helper")
595
+ - Architectural facts (e.g. "store exports are at the bottom of fastMarkets.ts")
596
+ - Only include genuinely useful, non-obvious facts. Do NOT repeat things already in the merge memory or repo docs.
597
+ - Leave discoveries as [] if nothing new and non-obvious was found.
598
+ 9. If merge memory entries were provided in the context above, evaluate each one:
599
+ - List the KEYS of entries you want to KEEP in keepMemoryKeys
600
+ - Drop entries that are stale, obvious from repo docs, or no longer relevant
601
+ - Keep entries that saved you time or would save time on similar future commits
602
+ - If no memory was provided, return keepMemoryKeys as []`;
589
603
  let response;
590
604
  if (diffChunks.length > 1) {
591
605
  await thread.run(firstPrompt);
@@ -613,7 +627,9 @@ You now have the complete diff. Analyze and produce your structured response.`,
613
627
  reasoning: "Could not parse structured output",
614
628
  applicationStrategy: "Manual review recommended",
615
629
  affectedComponents: [],
616
- opsNotes: []
630
+ opsNotes: [],
631
+ discoveries: [],
632
+ keepMemoryKeys: []
617
633
  });
618
634
  }
619
635
  async function applyCommit(opts) {
@@ -778,9 +794,43 @@ var init_codex = __esm(() => {
778
794
  type: "array",
779
795
  items: { type: "string" },
780
796
  description: "Operator action items ONLY if this commit requires something beyond a standard code deploy+restart. Examples: new env vars to add, database migrations to run, new services to deploy, infrastructure changes, config file updates on servers. Leave as empty array [] if no operator action is needed \u2014 a normal code deploy does NOT count."
797
+ },
798
+ discoveries: {
799
+ type: "array",
800
+ items: {
801
+ type: "object",
802
+ properties: {
803
+ type: {
804
+ type: "string",
805
+ enum: ["path_mapping", "pattern", "codebase", "architecture", "convention", "warning"],
806
+ description: "Category of discovery"
807
+ },
808
+ key: {
809
+ type: "string",
810
+ description: "Short unique key for dedup (e.g. 'frontend_path_prefix', 'pnl_shared_helpers')"
811
+ },
812
+ value: { type: "string", description: "The reusable fact (1-2 sentences max)" }
813
+ },
814
+ required: ["type", "key", "value"]
815
+ },
816
+ description: "Reusable learnings that would help analyze FUTURE commits. Only include genuinely useful, non-obvious facts. Examples: path mappings between source and target ('frontend/' in source = 'apps/frontend/' in target), key shared functions ('betExitValueLocal() is the canonical P&L helper'), architectural patterns ('store exports are at the bottom of the file'), conventions ('pt-BR locale used in all user-facing text'). Leave as [] if nothing non-obvious was discovered."
817
+ },
818
+ keepMemoryKeys: {
819
+ type: "array",
820
+ items: { type: "string" },
821
+ description: "From the merge memory provided in context, list the KEYS of entries that are still useful for future commits. Entries whose keys are NOT listed here will be garbage-collected. If no merge memory was provided, return []. Be selective \u2014 only keep entries that are genuinely useful going forward, not stale or obvious facts."
781
822
  }
782
823
  },
783
- required: ["summary", "alreadyInTarget", "reasoning", "applicationStrategy", "affectedComponents", "opsNotes"],
824
+ required: [
825
+ "summary",
826
+ "alreadyInTarget",
827
+ "reasoning",
828
+ "applicationStrategy",
829
+ "affectedComponents",
830
+ "opsNotes",
831
+ "discoveries",
832
+ "keepMemoryKeys"
833
+ ],
784
834
  additionalProperties: false
785
835
  };
786
836
  applyResultSchema = {
@@ -1200,9 +1250,114 @@ function findResumableRun(baseDir, workBranch) {
1200
1250
  }
1201
1251
  var init_audit = () => {};
1202
1252
 
1253
+ // src/memory.ts
1254
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
1255
+ import { join as join4 } from "path";
1256
+
1257
+ class MergeMemory {
1258
+ entries = [];
1259
+ filePath;
1260
+ maxEntries = 50;
1261
+ constructor(repoPath) {
1262
+ this.filePath = join4(repoPath, ".backmerge", "memory.jsonl");
1263
+ this.load();
1264
+ }
1265
+ load() {
1266
+ if (!existsSync4(this.filePath))
1267
+ return;
1268
+ try {
1269
+ const raw = readFileSync4(this.filePath, "utf-8");
1270
+ this.entries = raw.split(`
1271
+ `).filter(Boolean).map((line) => {
1272
+ try {
1273
+ return JSON.parse(line);
1274
+ } catch {
1275
+ return null;
1276
+ }
1277
+ }).filter((e) => e !== null);
1278
+ } catch {}
1279
+ }
1280
+ save() {
1281
+ const dir = join4(this.filePath, "..");
1282
+ mkdirSync2(dir, { recursive: true });
1283
+ writeFileSync2(this.filePath, this.entries.map((e) => JSON.stringify(e)).join(`
1284
+ `) + `
1285
+ `);
1286
+ }
1287
+ get all() {
1288
+ return this.entries;
1289
+ }
1290
+ get count() {
1291
+ return this.entries.length;
1292
+ }
1293
+ addDiscoveries(discoveries, commitHash) {
1294
+ let added = 0;
1295
+ for (const d of discoveries) {
1296
+ const existing = this.entries.find((e) => e.key === d.key);
1297
+ if (existing) {
1298
+ if (existing.value !== d.value) {
1299
+ existing.value = d.value;
1300
+ existing.source = commitHash;
1301
+ existing.ts = new Date().toISOString();
1302
+ }
1303
+ existing.useCount++;
1304
+ } else {
1305
+ this.entries.push({
1306
+ type: d.type,
1307
+ key: d.key,
1308
+ value: d.value,
1309
+ source: commitHash,
1310
+ ts: new Date().toISOString(),
1311
+ useCount: 1
1312
+ });
1313
+ added++;
1314
+ }
1315
+ }
1316
+ if (this.entries.length > this.maxEntries) {
1317
+ this.entries = this.entries.slice(-this.maxEntries);
1318
+ }
1319
+ this.save();
1320
+ return added;
1321
+ }
1322
+ applyGC(kept) {
1323
+ const keptKeys = new Set(kept.map((k) => k.key));
1324
+ const before = this.entries.length;
1325
+ const newEntries = [];
1326
+ for (const k of kept) {
1327
+ const existing = this.entries.find((e) => e.key === k.key);
1328
+ if (existing) {
1329
+ existing.value = k.value;
1330
+ existing.useCount++;
1331
+ newEntries.push(existing);
1332
+ } else {
1333
+ newEntries.push({
1334
+ type: "pattern",
1335
+ key: k.key,
1336
+ value: k.value,
1337
+ source: "gc",
1338
+ ts: new Date().toISOString(),
1339
+ useCount: 1
1340
+ });
1341
+ }
1342
+ }
1343
+ this.entries = newEntries;
1344
+ this.save();
1345
+ return before - this.entries.length;
1346
+ }
1347
+ toPromptBlock() {
1348
+ if (this.entries.length === 0)
1349
+ return "";
1350
+ const lines = this.entries.map((e) => `[${e.type}] ${e.key}: ${e.value}`);
1351
+ return `--- Merge memory (${this.entries.length} learnings from previous commits) ---
1352
+ ${lines.join(`
1353
+ `)}`;
1354
+ }
1355
+ }
1356
+ var init_memory = () => {};
1357
+
1203
1358
  // src/git.ts
1204
1359
  import simpleGit from "simple-git";
1205
- import { join as join4 } from "path";
1360
+ import { join as join5 } from "path";
1206
1361
  import { tmpdir } from "os";
1207
1362
  function createGit(cwd) {
1208
1363
  return simpleGit(cwd);
@@ -1262,7 +1417,7 @@ async function getDescendantCommitsSince(git, since, ref) {
1262
1417
  }
1263
1418
  function generateWorktreePath(repoName) {
1264
1419
  const id = Math.random().toString(36).slice(2, 8);
1265
- return join4(tmpdir(), `backmerge-${repoName}-${id}`);
1420
+ return join5(tmpdir(), `backmerge-${repoName}-${id}`);
1266
1421
  }
1267
1422
  async function createDetachedWorktree(git, path, ref) {
1268
1423
  await git.raw(["worktree", "add", "--detach", path, ref]);
@@ -1456,6 +1611,10 @@ async function runEngine(opts, cb) {
1456
1611
  for (const l of logs)
1457
1612
  cb.onLog(` ${l}`, "gray");
1458
1613
  }
1614
+ const memory = new MergeMemory(repoPath);
1615
+ if (memory.count > 0) {
1616
+ cb.onLog(`Merge memory: ${memory.count} entries from previous commits`, "cyan");
1617
+ }
1459
1618
  if (config.promptHints && config.promptHints.length > 0) {
1460
1619
  cb.onLog(`Active hints (${config.promptHints.length}):`, "cyan");
1461
1620
  for (const h of config.promptHints) {
@@ -1616,6 +1775,7 @@ async function runEngine(opts, cb) {
1616
1775
  maxAttempts: effectiveMaxAttempts,
1617
1776
  commitPrefix,
1618
1777
  workBranch: wbName,
1778
+ memory,
1619
1779
  workBranchHead: currentBranchHead
1620
1780
  });
1621
1781
  if (decision.kind === "applied" && decision.newCommitHash) {
@@ -1657,7 +1817,7 @@ async function processOneCommit(o) {
1657
1817
  try {
1658
1818
  touchedPaths = await getCommitFiles(o.git, commit.hash);
1659
1819
  } catch {}
1660
- const commitCtx = buildCommitContext(o.repoPath, o.repoCtx, o.config, touchedPaths, commit.message);
1820
+ const commitCtx = buildCommitContext(o.repoPath, o.repoCtx, o.config, touchedPaths, commit.message, o.memory.toPromptBlock());
1661
1821
  if (commitCtx.includedFiles.length > 0) {
1662
1822
  audit.writeRelevantDocs(commit.hash, 0, commitCtx.includedFiles.join(`
1663
1823
  `));
@@ -1685,6 +1845,19 @@ async function processOneCommit(o) {
1685
1845
  });
1686
1846
  audit.writeAnalysis(commit.hash, 1, analysis);
1687
1847
  cb.onAnalysis(commit, analysis);
1848
+ if (analysis.discoveries && analysis.discoveries.length > 0) {
1849
+ const added = o.memory.addDiscoveries(analysis.discoveries, commit.hash);
1850
+ if (added > 0) {
1851
+ cb.onLog(`Memory: +${added} new discoveries (${o.memory.count} total)`, "cyan");
1852
+ }
1853
+ }
1854
+ if (o.memory.count > 0 && analysis.keepMemoryKeys) {
1855
+ const kept = o.memory.all.filter((e) => analysis.keepMemoryKeys.includes(e.key));
1856
+ const gcCount = o.memory.applyGC(kept.map((e) => ({ key: e.key, value: e.value })));
1857
+ if (gcCount > 0) {
1858
+ cb.onLog(`Memory: GC'd ${gcCount} stale entries (${o.memory.count} remaining)`, "gray");
1859
+ }
1860
+ }
1688
1861
  } catch (e) {
1689
1862
  audit.error("analysis", commit.hash, commit.message, e.message);
1690
1863
  return mkFailed(commit, "analysis", e.message, start);
@@ -1974,6 +2147,7 @@ var init_engine = __esm(() => {
1974
2147
  init_codex();
1975
2148
  init_review();
1976
2149
  init_audit();
2150
+ init_memory();
1977
2151
  init_git();
1978
2152
  });
1979
2153
 
@@ -1983,11 +2157,11 @@ __export(exports_batch, {
1983
2157
  runBatch: () => runBatch
1984
2158
  });
1985
2159
  import chalk from "chalk";
1986
- import { readFileSync as readFileSync5 } from "fs";
1987
- import { join as join6 } from "path";
2160
+ import { readFileSync as readFileSync6 } from "fs";
2161
+ import { join as join7 } from "path";
1988
2162
  function getVersion() {
1989
2163
  try {
1990
- const pkg = JSON.parse(readFileSync5(join6(import.meta.dir, "..", "package.json"), "utf-8"));
2164
+ const pkg = JSON.parse(readFileSync6(join7(import.meta.dir, "..", "package.json"), "utf-8"));
1991
2165
  return pkg.version ?? "?";
1992
2166
  } catch {
1993
2167
  return "?";
@@ -2121,6 +2295,11 @@ async function runBatch(opts) {
2121
2295
  if (analysis.opsNotes.length > 0) {
2122
2296
  log(` ${chalk.yellow(" ops:")} ${analysis.opsNotes.join("; ")}`);
2123
2297
  }
2298
+ if (analysis.discoveries && analysis.discoveries.length > 0) {
2299
+ for (const d of analysis.discoveries) {
2300
+ log(` ${chalk.cyan(` \uD83D\uDCA1 [${d.type}] ${d.key}:`)} ${d.value}`);
2301
+ }
2302
+ }
2124
2303
  },
2125
2304
  onDecision(commit, decision) {
2126
2305
  if (jsonl)
@@ -2265,12 +2444,12 @@ import { useState, useEffect, useCallback, useRef } from "react";
2265
2444
  import { Box, Text, useInput, useApp, Static, Newline } from "ink";
2266
2445
  import SelectInput from "ink-select-input";
2267
2446
  import Spinner from "ink-spinner";
2268
- import { readFileSync as readFileSync4 } from "fs";
2269
- import { join as join5 } from "path";
2447
+ import { readFileSync as readFileSync5 } from "fs";
2448
+ import { join as join6 } from "path";
2270
2449
  import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
2271
2450
  var XAB_VERSION = (() => {
2272
2451
  try {
2273
- return JSON.parse(readFileSync4(join5(import.meta.dir, "..", "package.json"), "utf-8")).version ?? "?";
2452
+ return JSON.parse(readFileSync5(join6(import.meta.dir, "..", "package.json"), "utf-8")).version ?? "?";
2274
2453
  } catch {
2275
2454
  return "?";
2276
2455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,6 @@
33
33
  "ink-text-input": "^6.0.0",
34
34
  "react": "18.3.1",
35
35
  "simple-git": "^3.33.0",
36
- "xab": "^5.0.0"
36
+ "xab": "12"
37
37
  }
38
38
  }