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.
- package/dist/index.js +180 -42
- 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: "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
557
|
+
response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: analysisSchema });
|
|
504
558
|
}
|
|
505
|
-
return parseJson(
|
|
506
|
-
summary:
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
638
|
+
response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: applyResultSchema });
|
|
585
639
|
}
|
|
586
|
-
return parseJson(
|
|
640
|
+
return parseJson(response, {
|
|
587
641
|
applied: false,
|
|
588
642
|
filesChanged: [],
|
|
589
643
|
commitMessage: commitMsg,
|
|
590
|
-
notes:
|
|
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: "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
820
|
-
|
|
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
|
|
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 (
|
|
855
|
-
parts.push(`${
|
|
856
|
-
if (
|
|
857
|
-
parts.push(`${
|
|
858
|
-
if (
|
|
859
|
-
parts.push(`${
|
|
860
|
-
if (status.
|
|
861
|
-
parts.push(`${status.
|
|
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 });
|