xab 7.0.0 → 9.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 +180 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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
  });
@@ -560,7 +614,7 @@ ${diffChunks[0]}
560
614
  \`\`\`
561
615
 
562
616
  ${diffChunks.length > 1 ? "I will send the remaining diff parts next. Read them all before applying." : instructions}`;
563
- let turn;
617
+ let response;
564
618
  if (diffChunks.length > 1) {
565
619
  await thread.run(firstPrompt);
566
620
  for (let i = 1;i < diffChunks.length - 1; i++) {
@@ -572,22 +626,22 @@ ${diffChunks[i]}
572
626
  Continue reading. More parts coming.`);
573
627
  }
574
628
  const lastIdx = diffChunks.length - 1;
575
- 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):
576
630
  \`\`\`diff
577
631
  ${diffChunks[lastIdx]}
578
632
  \`\`\`
579
633
 
580
634
  You now have the complete diff.
581
635
 
582
- ${instructions}`, { outputSchema: applyResultSchema });
636
+ ${instructions}`, opts.onProgress, { outputSchema: applyResultSchema });
583
637
  } else {
584
- turn = await thread.run(firstPrompt, { outputSchema: applyResultSchema });
638
+ response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: applyResultSchema });
585
639
  }
586
- return parseJson(turn.finalResponse, {
640
+ return parseJson(response, {
587
641
  applied: false,
588
642
  filesChanged: [],
589
643
  commitMessage: commitMsg,
590
- notes: turn.finalResponse.slice(0, 1000),
644
+ notes: response.slice(0, 1000),
591
645
  adaptations: ""
592
646
  });
593
647
  }
@@ -595,7 +649,7 @@ async function fixFromReview(opts) {
595
649
  const codex = new Codex;
596
650
  const thread = codex.startThread({
597
651
  workingDirectory: opts.worktreePath,
598
- sandboxMode: "workspace-write",
652
+ sandboxMode: "danger-full-access",
599
653
  model: "gpt-5.4",
600
654
  modelReasoningEffort: "high"
601
655
  });
@@ -714,32 +768,47 @@ function writeReviewPacket(audit, packet, attempt) {
714
768
  audit.writeAppliedPatch(commitHash, attempt, packet.appliedDiff);
715
769
  }
716
770
  }
717
- async function reviewAppliedDiff(worktreePath, packet) {
771
+ async function reviewAppliedDiff(worktreePath, packet, onProgress) {
718
772
  const strictnessInstructions = {
719
773
  strict: "Be very strict. Any questionable change should be rejected. Err on the side of caution.",
720
774
  normal: "Be thorough but reasonable. Reject clear issues, accept minor style differences.",
721
775
  lenient: "Focus on correctness and safety. Accept reasonable adaptations even if imperfect."
722
776
  };
723
- const prompt = `You are reviewing a curated merge. A commit from "${packet.sourceBranch}" was applied to a branch based on "${packet.targetBranch}".
777
+ const prompt = `You are reviewing a curated merge commit. Codex adapted a source commit from "${packet.sourceBranch}" and applied it to this worktree (based on "${packet.targetBranch}").
724
778
 
725
- ## Codex analysis of the source commit
726
- Summary: ${packet.analysis.summary}
727
- Decision: alreadyInTarget=${packet.analysis.alreadyInTarget}
728
- Strategy: ${packet.analysis.applicationStrategy}
729
- Affected components: ${packet.analysis.affectedComponents.join(", ")}
779
+ Your job: review the NEW commit Codex created. Verify it is correct, clean, and faithful to the source commit's intent while respecting the target's architecture.
730
780
 
731
- ## Source commit
781
+ ## Source commit (what was being backmerged)
732
782
  Hash: ${packet.commitHash}
733
783
  Message: ${packet.commitMessage}
784
+ Branch: ${packet.sourceBranch}
734
785
 
735
- ## Applied diff (what was actually committed):
786
+ ### Original source diff:
787
+ \`\`\`diff
788
+ ${packet.sourcePatch.slice(0, 15000)}
789
+ \`\`\`
790
+
791
+ ## Codex's analysis
792
+ Summary: ${packet.analysis.summary}
793
+ Already in target: ${packet.analysis.alreadyInTarget}
794
+ Strategy used: ${packet.analysis.applicationStrategy}
795
+ Components: ${packet.analysis.affectedComponents.join(", ")}
796
+
797
+ ## Codex's applied commit (what you are reviewing)
798
+ ${packet.newCommitHash ? `Commit: ${packet.newCommitHash} (in this worktree)
799
+ Run \`git show ${packet.newCommitHash.slice(0, 8)}\` to inspect it.` : `Run \`git show HEAD\` to inspect it.`}
800
+
801
+ ### Applied diff:
736
802
  \`\`\`diff
737
803
  ${packet.appliedDiff.slice(0, 30000)}
738
804
  \`\`\`
739
805
 
740
- ## Diff stat:
806
+ ### Diff stat:
741
807
  ${packet.appliedDiffStat}
742
808
 
809
+ ## Key question: does the applied commit correctly adapt the source commit's intent?
810
+ Compare the source diff with the applied diff. The applied commit should capture the same behavior/fix but adapted for the target branch's codebase structure.
811
+
743
812
  ${packet.repoContext ? `## Repository context
744
813
  ${packet.repoContext}
745
814
  ` : ""}
@@ -800,14 +869,20 @@ You can:
800
869
  - Run tests, linters, type-checkers, and build commands via Bash to verify correctness
801
870
  - Run any read-only shell command (cat, ls, git diff, git log, etc.)
802
871
 
803
- You MUST NOT modify the worktree in any way. No file writes, no git commits, no destructive commands.
872
+ You MUST NOT modify the worktree. Specifically:
873
+ - NO file writes, edits, or creates
874
+ - NO git commit, git add, git reset, or any git mutation
875
+ - NO npm install, bun install, yarn install, pnpm install, or any package manager install
876
+ - NO rm, mv, cp, or any file mutation commands
877
+ - NO pip install, cargo build, go get, or anything that writes to disk
804
878
 
805
879
  Testing guidelines:
880
+ - Only run tests that work without installing dependencies (assume deps are already installed if node_modules exists)
806
881
  - Only run tests that work without API keys, secrets, or external service connections
807
882
  - Before running a test, check if it needs env vars by reading the test file or relevant .env.example
808
- - If a test needs keys, only run it if you can see a .env file with those vars already populated
809
883
  - Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
810
884
  - Avoid: integration tests hitting external APIs, tests requiring running databases/services
885
+ - Do NOT run bun install, npm install, or equivalent \u2014 deps are already there if they exist
811
886
  - If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
812
887
 
813
888
  Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
@@ -815,16 +890,51 @@ Your objections will be sent back to the apply agent for fixing, so be specific
815
890
  });
816
891
  let resultText = "";
817
892
  for await (const message of q) {
893
+ if (onProgress && message.type === "assistant") {
894
+ const betaMsg = message.message;
895
+ const content = betaMsg?.content;
896
+ if (content) {
897
+ for (const block of content) {
898
+ if (block.type === "tool_use") {
899
+ const name = block.name;
900
+ const input = block.input;
901
+ if (name === "Bash") {
902
+ onProgress("review", `$ ${(input.command ?? "").slice(0, 120)}`);
903
+ } else if (name === "Read") {
904
+ onProgress("review", `read ${(input.file_path ?? "").replace(worktreePath + "/", "")}`);
905
+ } else if (name === "Grep") {
906
+ onProgress("review", `grep "${(input.pattern ?? "").slice(0, 60)}"`);
907
+ } else if (name === "Glob") {
908
+ onProgress("review", `glob ${(input.pattern ?? "").slice(0, 60)}`);
909
+ } else {
910
+ onProgress("review", `${name}`);
911
+ }
912
+ } else if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
913
+ const firstLine = block.text.split(`
914
+ `)[0].slice(0, 120);
915
+ if (firstLine)
916
+ onProgress("review", firstLine);
917
+ }
918
+ }
919
+ }
920
+ }
818
921
  if (message.type === "result") {
819
- if ("result" in message) {
820
- resultText = message.result;
922
+ const msg = message;
923
+ if (msg.structured_output) {
924
+ resultText = typeof msg.structured_output === "string" ? msg.structured_output : JSON.stringify(msg.structured_output);
925
+ } else if (msg.result) {
926
+ resultText = msg.result;
821
927
  }
822
928
  break;
823
929
  }
824
930
  }
825
931
  if (!resultText) {
932
+ if (onProgress)
933
+ onProgress("review", "WARNING: no structured_output or result in review response");
826
934
  return { approved: false, issues: ["Review produced no output"], summary: "Review failed", confidence: "low" };
827
935
  }
936
+ if (onProgress)
937
+ onProgress("review", `got ${resultText.length} chars of review output`);
828
938
  try {
829
939
  return JSON.parse(resultText);
830
940
  } catch {
@@ -848,17 +958,22 @@ async function verifyReviewIntegrity(wtGit, expectedHead) {
848
958
  return `Review mutated HEAD: expected ${expectedHead.slice(0, 8)}, got ${currentHead.slice(0, 8)}`;
849
959
  }
850
960
  const status = await wtGit.status();
851
- const dirty = status.modified.length + status.created.length + status.deleted.length + status.not_added.length + status.conflicted.length;
961
+ 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/");
962
+ const modified = status.modified.filter((f) => !isInfra(f));
963
+ const created = status.created.filter((f) => !isInfra(f));
964
+ const deleted = status.deleted.filter((f) => !isInfra(f));
965
+ const notAdded = status.not_added.filter((f) => !isInfra(f));
966
+ const dirty = modified.length + created.length + deleted.length + notAdded.length + status.conflicted.length;
852
967
  if (dirty > 0) {
853
968
  const parts = [];
854
- if (status.modified.length)
855
- parts.push(`${status.modified.length} modified`);
856
- if (status.created.length)
857
- parts.push(`${status.created.length} staged`);
858
- if (status.not_added.length)
859
- parts.push(`${status.not_added.length} untracked`);
860
- if (status.deleted.length)
861
- parts.push(`${status.deleted.length} deleted`);
969
+ if (modified.length)
970
+ parts.push(`${modified.length} modified`);
971
+ if (notAdded.length)
972
+ parts.push(`${notAdded.length} untracked`);
973
+ if (deleted.length)
974
+ parts.push(`${deleted.length} deleted`);
975
+ if (status.conflicted.length)
976
+ parts.push(`${status.conflicted.length} conflicted`);
862
977
  return `Review left dirty worktree: ${parts.join(", ")}`;
863
978
  }
864
979
  return null;
@@ -1501,7 +1616,8 @@ async function processOneCommit(o) {
1501
1616
  sourceBranch: o.sourceRef,
1502
1617
  targetBranch: o.targetRef,
1503
1618
  sourceLatestDiff: o.sourceLatestDiff,
1504
- repoContext: commitCtx.promptBlock
1619
+ repoContext: commitCtx.promptBlock,
1620
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("analyze", `[${phase}] ${msg}`) : undefined
1505
1621
  });
1506
1622
  audit.writeAnalysis(commit.hash, 1, analysis);
1507
1623
  cb.onAnalysis(commit, analysis);
@@ -1564,7 +1680,8 @@ async function processOneCommit(o) {
1564
1680
  sourceBranch: o.sourceRef,
1565
1681
  targetBranch: o.targetRef,
1566
1682
  repoContext: commitCtx.promptBlock,
1567
- commitPrefix: o.commitPrefix
1683
+ commitPrefix: o.commitPrefix,
1684
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("apply", `[${phase}] ${msg}`) : undefined
1568
1685
  });
1569
1686
  audit.writeAnalysis(commit.hash, attempt, { ...analysis, applyResult });
1570
1687
  } catch (e) {
@@ -1617,6 +1734,7 @@ async function processOneCommit(o) {
1617
1734
  packet = {
1618
1735
  commitHash: commit.hash,
1619
1736
  commitMessage: commit.message,
1737
+ newCommitHash: validation.newCommitHash ?? undefined,
1620
1738
  sourceBranch: o.sourceRef,
1621
1739
  targetBranch: o.targetRef,
1622
1740
  analysis,
@@ -1630,7 +1748,7 @@ async function processOneCommit(o) {
1630
1748
  };
1631
1749
  writeReviewPacket(audit, packet, attempt);
1632
1750
  const headBeforeReview = await getHead(o.wtGit);
1633
- reviewResult = await reviewAppliedDiff(o.wtPath, packet);
1751
+ reviewResult = await reviewAppliedDiff(o.wtPath, packet, o.cb.onProgress);
1634
1752
  audit.writeReviewResult(commit.hash, attempt, reviewResult);
1635
1753
  const mutation = await verifyReviewIntegrity(o.wtGit, headBeforeReview);
1636
1754
  if (mutation) {
@@ -1682,7 +1800,7 @@ async function processOneCommit(o) {
1682
1800
  appliedDiffStat: fixedStat
1683
1801
  };
1684
1802
  const headBeforeReReview = await getHead(o.wtGit);
1685
- reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket);
1803
+ reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket, o.cb.onProgress);
1686
1804
  audit.writeReviewResult(commit.hash, attempt, {
1687
1805
  ...reviewResult,
1688
1806
  fixRound
@@ -1877,6 +1995,26 @@ async function runBatch(opts) {
1877
1995
  log(` ${divider()}`);
1878
1996
  log("");
1879
1997
  const cb = {
1998
+ onProgress(phase, msg) {
1999
+ if (jsonl)
2000
+ emitJsonl({ event: "progress", phase, msg });
2001
+ let icon;
2002
+ switch (phase) {
2003
+ case "analyze":
2004
+ icon = chalk.blue("\u25C6");
2005
+ break;
2006
+ case "apply":
2007
+ icon = chalk.green("\u25B8");
2008
+ break;
2009
+ case "review":
2010
+ icon = chalk.magenta("\u25CF");
2011
+ break;
2012
+ default:
2013
+ icon = chalk.dim("\xB7");
2014
+ break;
2015
+ }
2016
+ log(` ${ts()} ${icon} ${chalk.dim(msg)}`);
2017
+ },
1880
2018
  onLog(msg, color) {
1881
2019
  if (jsonl)
1882
2020
  emitJsonl({ event: "log", msg, color });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xab",
3
- "version": "7.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {