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.
- package/dist/index.js +192 -13
- 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: [
|
|
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
|
|
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
|
|
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
|
|
1987
|
-
import { join as
|
|
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(
|
|
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
|
|
2269
|
-
import { join as
|
|
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(
|
|
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": "
|
|
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": "
|
|
36
|
+
"xab": "12"
|
|
37
37
|
}
|
|
38
38
|
}
|