xab 7.0.0 → 8.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 +132 -31
  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,7 +768,7 @@ 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.",
@@ -800,14 +854,20 @@ You can:
800
854
  - Run tests, linters, type-checkers, and build commands via Bash to verify correctness
801
855
  - Run any read-only shell command (cat, ls, git diff, git log, etc.)
802
856
 
803
- You MUST NOT modify the worktree in any way. No file writes, no git commits, no destructive commands.
857
+ You MUST NOT modify the worktree. Specifically:
858
+ - NO file writes, edits, or creates
859
+ - NO git commit, git add, git reset, or any git mutation
860
+ - NO npm install, bun install, yarn install, pnpm install, or any package manager install
861
+ - NO rm, mv, cp, or any file mutation commands
862
+ - NO pip install, cargo build, go get, or anything that writes to disk
804
863
 
805
864
  Testing guidelines:
865
+ - Only run tests that work without installing dependencies (assume deps are already installed if node_modules exists)
806
866
  - Only run tests that work without API keys, secrets, or external service connections
807
867
  - 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
868
  - Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
810
869
  - Avoid: integration tests hitting external APIs, tests requiring running databases/services
870
+ - Do NOT run bun install, npm install, or equivalent \u2014 deps are already there if they exist
811
871
  - If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
812
872
 
813
873
  Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
@@ -815,6 +875,34 @@ Your objections will be sent back to the apply agent for fixing, so be specific
815
875
  });
816
876
  let resultText = "";
817
877
  for await (const message of q) {
878
+ if (onProgress && message.type === "assistant") {
879
+ const betaMsg = message.message;
880
+ const content = betaMsg?.content;
881
+ if (content) {
882
+ for (const block of content) {
883
+ if (block.type === "tool_use") {
884
+ const name = block.name;
885
+ const input = block.input;
886
+ if (name === "Bash") {
887
+ onProgress("review", `$ ${(input.command ?? "").slice(0, 120)}`);
888
+ } else if (name === "Read") {
889
+ onProgress("review", `read ${(input.file_path ?? "").replace(worktreePath + "/", "")}`);
890
+ } else if (name === "Grep") {
891
+ onProgress("review", `grep "${(input.pattern ?? "").slice(0, 60)}"`);
892
+ } else if (name === "Glob") {
893
+ onProgress("review", `glob ${(input.pattern ?? "").slice(0, 60)}`);
894
+ } else {
895
+ onProgress("review", `${name}`);
896
+ }
897
+ } else if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
898
+ const firstLine = block.text.split(`
899
+ `)[0].slice(0, 120);
900
+ if (firstLine)
901
+ onProgress("review", firstLine);
902
+ }
903
+ }
904
+ }
905
+ }
818
906
  if (message.type === "result") {
819
907
  if ("result" in message) {
820
908
  resultText = message.result;
@@ -848,17 +936,22 @@ async function verifyReviewIntegrity(wtGit, expectedHead) {
848
936
  return `Review mutated HEAD: expected ${expectedHead.slice(0, 8)}, got ${currentHead.slice(0, 8)}`;
849
937
  }
850
938
  const status = await wtGit.status();
851
- const dirty = status.modified.length + status.created.length + status.deleted.length + status.not_added.length + status.conflicted.length;
939
+ 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/");
940
+ const modified = status.modified.filter((f) => !isInfra(f));
941
+ const created = status.created.filter((f) => !isInfra(f));
942
+ const deleted = status.deleted.filter((f) => !isInfra(f));
943
+ const notAdded = status.not_added.filter((f) => !isInfra(f));
944
+ const dirty = modified.length + created.length + deleted.length + notAdded.length + status.conflicted.length;
852
945
  if (dirty > 0) {
853
946
  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`);
947
+ if (modified.length)
948
+ parts.push(`${modified.length} modified`);
949
+ if (notAdded.length)
950
+ parts.push(`${notAdded.length} untracked`);
951
+ if (deleted.length)
952
+ parts.push(`${deleted.length} deleted`);
953
+ if (status.conflicted.length)
954
+ parts.push(`${status.conflicted.length} conflicted`);
862
955
  return `Review left dirty worktree: ${parts.join(", ")}`;
863
956
  }
864
957
  return null;
@@ -1501,7 +1594,8 @@ async function processOneCommit(o) {
1501
1594
  sourceBranch: o.sourceRef,
1502
1595
  targetBranch: o.targetRef,
1503
1596
  sourceLatestDiff: o.sourceLatestDiff,
1504
- repoContext: commitCtx.promptBlock
1597
+ repoContext: commitCtx.promptBlock,
1598
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("analyze", `[${phase}] ${msg}`) : undefined
1505
1599
  });
1506
1600
  audit.writeAnalysis(commit.hash, 1, analysis);
1507
1601
  cb.onAnalysis(commit, analysis);
@@ -1564,7 +1658,8 @@ async function processOneCommit(o) {
1564
1658
  sourceBranch: o.sourceRef,
1565
1659
  targetBranch: o.targetRef,
1566
1660
  repoContext: commitCtx.promptBlock,
1567
- commitPrefix: o.commitPrefix
1661
+ commitPrefix: o.commitPrefix,
1662
+ onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("apply", `[${phase}] ${msg}`) : undefined
1568
1663
  });
1569
1664
  audit.writeAnalysis(commit.hash, attempt, { ...analysis, applyResult });
1570
1665
  } catch (e) {
@@ -1630,7 +1725,7 @@ async function processOneCommit(o) {
1630
1725
  };
1631
1726
  writeReviewPacket(audit, packet, attempt);
1632
1727
  const headBeforeReview = await getHead(o.wtGit);
1633
- reviewResult = await reviewAppliedDiff(o.wtPath, packet);
1728
+ reviewResult = await reviewAppliedDiff(o.wtPath, packet, o.cb.onProgress);
1634
1729
  audit.writeReviewResult(commit.hash, attempt, reviewResult);
1635
1730
  const mutation = await verifyReviewIntegrity(o.wtGit, headBeforeReview);
1636
1731
  if (mutation) {
@@ -1682,7 +1777,7 @@ async function processOneCommit(o) {
1682
1777
  appliedDiffStat: fixedStat
1683
1778
  };
1684
1779
  const headBeforeReReview = await getHead(o.wtGit);
1685
- reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket);
1780
+ reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket, o.cb.onProgress);
1686
1781
  audit.writeReviewResult(commit.hash, attempt, {
1687
1782
  ...reviewResult,
1688
1783
  fixRound
@@ -1877,6 +1972,12 @@ async function runBatch(opts) {
1877
1972
  log(` ${divider()}`);
1878
1973
  log("");
1879
1974
  const cb = {
1975
+ onProgress(phase, msg) {
1976
+ if (jsonl)
1977
+ emitJsonl({ event: "progress", phase, msg });
1978
+ const icon = phase === "apply" ? chalk.green("\u25B8") : chalk.blue("\u25B8");
1979
+ log(` ${ts()} ${icon} ${chalk.dim(msg)}`);
1980
+ },
1880
1981
  onLog(msg, color) {
1881
1982
  if (jsonl)
1882
1983
  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": "8.0.0",
4
4
  "description": "AI-powered curated branch reconciliation engine",
5
5
  "type": "module",
6
6
  "bin": {