xab 6.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.
- package/dist/index.js +174 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ var init_config = __esm(() => {
|
|
|
43
43
|
promptHints: [],
|
|
44
44
|
pathRemaps: [],
|
|
45
45
|
reviewStrictness: "normal",
|
|
46
|
-
maxAttempts:
|
|
46
|
+
maxAttempts: undefined,
|
|
47
47
|
commitPrefix: "backmerge:"
|
|
48
48
|
};
|
|
49
49
|
});
|
|
@@ -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
|
});
|
|
@@ -543,7 +597,9 @@ After making all file changes, you MUST run these two commands as your FINAL act
|
|
|
543
597
|
If you do not run both commands, your work will be discarded. This is not optional.
|
|
544
598
|
The validation system checks for exactly one new git commit. Zero commits = failure.
|
|
545
599
|
|
|
546
|
-
Report what you did
|
|
600
|
+
Report what you did.
|
|
601
|
+
|
|
602
|
+
MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
|
|
547
603
|
const firstPrompt = `${MERGE_PREAMBLE}
|
|
548
604
|
${opts.repoContext ? `## Repository context
|
|
549
605
|
${opts.repoContext}
|
|
@@ -558,7 +614,7 @@ ${diffChunks[0]}
|
|
|
558
614
|
\`\`\`
|
|
559
615
|
|
|
560
616
|
${diffChunks.length > 1 ? "I will send the remaining diff parts next. Read them all before applying." : instructions}`;
|
|
561
|
-
let
|
|
617
|
+
let response;
|
|
562
618
|
if (diffChunks.length > 1) {
|
|
563
619
|
await thread.run(firstPrompt);
|
|
564
620
|
for (let i = 1;i < diffChunks.length - 1; i++) {
|
|
@@ -570,22 +626,22 @@ ${diffChunks[i]}
|
|
|
570
626
|
Continue reading. More parts coming.`);
|
|
571
627
|
}
|
|
572
628
|
const lastIdx = diffChunks.length - 1;
|
|
573
|
-
|
|
629
|
+
response = await runStreamedWithProgress(thread, `### Diff (part ${lastIdx + 1}/${diffChunks.length} \u2014 final):
|
|
574
630
|
\`\`\`diff
|
|
575
631
|
${diffChunks[lastIdx]}
|
|
576
632
|
\`\`\`
|
|
577
633
|
|
|
578
634
|
You now have the complete diff.
|
|
579
635
|
|
|
580
|
-
${instructions}`, { outputSchema: applyResultSchema });
|
|
636
|
+
${instructions}`, opts.onProgress, { outputSchema: applyResultSchema });
|
|
581
637
|
} else {
|
|
582
|
-
|
|
638
|
+
response = await runStreamedWithProgress(thread, firstPrompt, opts.onProgress, { outputSchema: applyResultSchema });
|
|
583
639
|
}
|
|
584
|
-
return parseJson(
|
|
640
|
+
return parseJson(response, {
|
|
585
641
|
applied: false,
|
|
586
642
|
filesChanged: [],
|
|
587
643
|
commitMessage: commitMsg,
|
|
588
|
-
notes:
|
|
644
|
+
notes: response.slice(0, 1000),
|
|
589
645
|
adaptations: ""
|
|
590
646
|
});
|
|
591
647
|
}
|
|
@@ -593,7 +649,7 @@ async function fixFromReview(opts) {
|
|
|
593
649
|
const codex = new Codex;
|
|
594
650
|
const thread = codex.startThread({
|
|
595
651
|
workingDirectory: opts.worktreePath,
|
|
596
|
-
sandboxMode: "
|
|
652
|
+
sandboxMode: "danger-full-access",
|
|
597
653
|
model: "gpt-5.4",
|
|
598
654
|
modelReasoningEffort: "high"
|
|
599
655
|
});
|
|
@@ -622,7 +678,9 @@ After making all fixes, you MUST run these two commands as your FINAL action:
|
|
|
622
678
|
|
|
623
679
|
If you do not run both commands, your fixes will be discarded. This is not optional.
|
|
624
680
|
|
|
625
|
-
Report what you fixed
|
|
681
|
+
Report what you fixed.
|
|
682
|
+
|
|
683
|
+
MAKE SURE TO ACTUALLY GIT COMMIT, NOT JUST MODIFY FILES.`;
|
|
626
684
|
const turn = await thread.run(prompt, { outputSchema: applyResultSchema });
|
|
627
685
|
return parseJson(turn.finalResponse, {
|
|
628
686
|
applied: false,
|
|
@@ -710,7 +768,7 @@ function writeReviewPacket(audit, packet, attempt) {
|
|
|
710
768
|
audit.writeAppliedPatch(commitHash, attempt, packet.appliedDiff);
|
|
711
769
|
}
|
|
712
770
|
}
|
|
713
|
-
async function reviewAppliedDiff(worktreePath, packet) {
|
|
771
|
+
async function reviewAppliedDiff(worktreePath, packet, onProgress) {
|
|
714
772
|
const strictnessInstructions = {
|
|
715
773
|
strict: "Be very strict. Any questionable change should be rejected. Err on the side of caution.",
|
|
716
774
|
normal: "Be thorough but reasonable. Reject clear issues, accept minor style differences.",
|
|
@@ -783,7 +841,7 @@ If you have ANY objections, be specific about what's wrong and how to fix it. Yo
|
|
|
783
841
|
options: {
|
|
784
842
|
cwd: worktreePath,
|
|
785
843
|
model: "claude-opus-4-6",
|
|
786
|
-
maxTurns:
|
|
844
|
+
maxTurns: undefined,
|
|
787
845
|
permissionMode: "default",
|
|
788
846
|
outputFormat: reviewSchema,
|
|
789
847
|
tools: ["Read", "Glob", "Grep", "Bash"],
|
|
@@ -796,14 +854,20 @@ You can:
|
|
|
796
854
|
- Run tests, linters, type-checkers, and build commands via Bash to verify correctness
|
|
797
855
|
- Run any read-only shell command (cat, ls, git diff, git log, etc.)
|
|
798
856
|
|
|
799
|
-
You MUST NOT modify the worktree
|
|
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
|
|
800
863
|
|
|
801
864
|
Testing guidelines:
|
|
865
|
+
- Only run tests that work without installing dependencies (assume deps are already installed if node_modules exists)
|
|
802
866
|
- Only run tests that work without API keys, secrets, or external service connections
|
|
803
867
|
- Before running a test, check if it needs env vars by reading the test file or relevant .env.example
|
|
804
|
-
- If a test needs keys, only run it if you can see a .env file with those vars already populated
|
|
805
868
|
- Prefer: type-checks (tsc --noEmit), linters (eslint), unit tests, build checks (forge build, go build)
|
|
806
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
|
|
807
871
|
- If you can't determine whether a test needs keys, skip it \u2014 don't run and fail
|
|
808
872
|
|
|
809
873
|
Your objections will be sent back to the apply agent for fixing, so be specific and actionable.`
|
|
@@ -811,6 +875,34 @@ Your objections will be sent back to the apply agent for fixing, so be specific
|
|
|
811
875
|
});
|
|
812
876
|
let resultText = "";
|
|
813
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
|
+
}
|
|
814
906
|
if (message.type === "result") {
|
|
815
907
|
if ("result" in message) {
|
|
816
908
|
resultText = message.result;
|
|
@@ -844,17 +936,22 @@ async function verifyReviewIntegrity(wtGit, expectedHead) {
|
|
|
844
936
|
return `Review mutated HEAD: expected ${expectedHead.slice(0, 8)}, got ${currentHead.slice(0, 8)}`;
|
|
845
937
|
}
|
|
846
938
|
const status = await wtGit.status();
|
|
847
|
-
const
|
|
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;
|
|
848
945
|
if (dirty > 0) {
|
|
849
946
|
const parts = [];
|
|
850
|
-
if (
|
|
851
|
-
parts.push(`${
|
|
852
|
-
if (
|
|
853
|
-
parts.push(`${
|
|
854
|
-
if (
|
|
855
|
-
parts.push(`${
|
|
856
|
-
if (status.
|
|
857
|
-
parts.push(`${status.
|
|
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`);
|
|
858
955
|
return `Review left dirty worktree: ${parts.join(", ")}`;
|
|
859
956
|
}
|
|
860
957
|
return null;
|
|
@@ -1173,12 +1270,12 @@ async function validateApply(worktreeGit, beforeHash) {
|
|
|
1173
1270
|
errors.push(`Failed to check commits: ${e.message}`);
|
|
1174
1271
|
}
|
|
1175
1272
|
const status = await worktreeGit.status();
|
|
1176
|
-
const
|
|
1177
|
-
const modified = status.modified.filter((f) => !
|
|
1178
|
-
const created = status.created.filter((f) => !
|
|
1179
|
-
const deleted = status.deleted.filter((f) => !
|
|
1180
|
-
const notAdded = status.not_added.filter((f) => !
|
|
1181
|
-
const conflicted = status.conflicted.filter((f) => !
|
|
1273
|
+
const isInfra = (f) => f.startsWith(".backmerge/") || f.startsWith(".backmerge\\") || f.startsWith(".git-local/") || f.startsWith(".git-local\\");
|
|
1274
|
+
const modified = status.modified.filter((f) => !isInfra(f));
|
|
1275
|
+
const created = status.created.filter((f) => !isInfra(f));
|
|
1276
|
+
const deleted = status.deleted.filter((f) => !isInfra(f));
|
|
1277
|
+
const notAdded = status.not_added.filter((f) => !isInfra(f));
|
|
1278
|
+
const conflicted = status.conflicted.filter((f) => !isInfra(f));
|
|
1182
1279
|
const worktreeClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && conflicted.length === 0 && notAdded.length === 0;
|
|
1183
1280
|
const dirtyFiles = [];
|
|
1184
1281
|
if (!worktreeClean) {
|
|
@@ -1228,7 +1325,7 @@ async function getAppliedDiffStat(worktreeGit, beforeHash) {
|
|
|
1228
1325
|
}
|
|
1229
1326
|
async function resetHard(git, ref) {
|
|
1230
1327
|
await git.raw(["reset", "--hard", ref]);
|
|
1231
|
-
await git.raw(["clean", "-fd", "--exclude=.backmerge"]);
|
|
1328
|
+
await git.raw(["clean", "-fd", "--exclude=.backmerge", "--exclude=.git-local"]);
|
|
1232
1329
|
}
|
|
1233
1330
|
async function fetchOrigin(git) {
|
|
1234
1331
|
await git.fetch(["--all", "--prune"]);
|
|
@@ -1268,7 +1365,7 @@ async function runEngine(opts, cb) {
|
|
|
1268
1365
|
autoSkip = true
|
|
1269
1366
|
} = opts;
|
|
1270
1367
|
const config = loadConfig(repoPath, opts.configPath);
|
|
1271
|
-
const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ??
|
|
1368
|
+
const effectiveMaxAttempts = opts.maxAttempts ?? config.maxAttempts ?? Infinity;
|
|
1272
1369
|
const effectiveWorkBranch = opts.workBranch ?? config.workBranch;
|
|
1273
1370
|
const commitPrefix = config.commitPrefix ?? "backmerge:";
|
|
1274
1371
|
if (!checkCodexInstalled())
|
|
@@ -1497,7 +1594,8 @@ async function processOneCommit(o) {
|
|
|
1497
1594
|
sourceBranch: o.sourceRef,
|
|
1498
1595
|
targetBranch: o.targetRef,
|
|
1499
1596
|
sourceLatestDiff: o.sourceLatestDiff,
|
|
1500
|
-
repoContext: commitCtx.promptBlock
|
|
1597
|
+
repoContext: commitCtx.promptBlock,
|
|
1598
|
+
onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("analyze", `[${phase}] ${msg}`) : undefined
|
|
1501
1599
|
});
|
|
1502
1600
|
audit.writeAnalysis(commit.hash, 1, analysis);
|
|
1503
1601
|
cb.onAnalysis(commit, analysis);
|
|
@@ -1548,7 +1646,7 @@ async function processOneCommit(o) {
|
|
|
1548
1646
|
}
|
|
1549
1647
|
for (let attempt = 1;attempt <= o.maxAttempts; attempt++) {
|
|
1550
1648
|
const headBefore = await getHead(o.wtGit);
|
|
1551
|
-
cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt}
|
|
1649
|
+
cb.onStatus(`Applying ${commit.hash.slice(0, 8)} (attempt ${attempt})...`);
|
|
1552
1650
|
let applyResult;
|
|
1553
1651
|
try {
|
|
1554
1652
|
applyResult = await applyCommit({
|
|
@@ -1560,7 +1658,8 @@ async function processOneCommit(o) {
|
|
|
1560
1658
|
sourceBranch: o.sourceRef,
|
|
1561
1659
|
targetBranch: o.targetRef,
|
|
1562
1660
|
repoContext: commitCtx.promptBlock,
|
|
1563
|
-
commitPrefix: o.commitPrefix
|
|
1661
|
+
commitPrefix: o.commitPrefix,
|
|
1662
|
+
onProgress: o.cb.onProgress ? (phase, msg) => o.cb.onProgress("apply", `[${phase}] ${msg}`) : undefined
|
|
1564
1663
|
});
|
|
1565
1664
|
audit.writeAnalysis(commit.hash, attempt, { ...analysis, applyResult });
|
|
1566
1665
|
} catch (e) {
|
|
@@ -1571,7 +1670,26 @@ async function processOneCommit(o) {
|
|
|
1571
1670
|
continue;
|
|
1572
1671
|
}
|
|
1573
1672
|
cb.onStatus(`Validating ${commit.hash.slice(0, 8)}...`);
|
|
1574
|
-
|
|
1673
|
+
let validation = await validateApply(o.wtGit, headBefore);
|
|
1674
|
+
if (!validation.valid && validation.newCommitCount === 0 && !validation.worktreeClean && validation.dirtyFiles.length > 0) {
|
|
1675
|
+
const realChanges = validation.dirtyFiles.filter((f) => !f.includes(".git-local/"));
|
|
1676
|
+
if (realChanges.length > 0) {
|
|
1677
|
+
cb.onLog(`Codex left ${realChanges.length} changed files without committing \u2014 creating rescue commit`, "yellow");
|
|
1678
|
+
for (const f of realChanges)
|
|
1679
|
+
cb.onLog(` ${f}`, "yellow");
|
|
1680
|
+
const commitMsg = `${o.commitPrefix} ${commit.message} (from ${commit.hash.slice(0, 8)})`;
|
|
1681
|
+
try {
|
|
1682
|
+
await o.wtGit.raw(["add", "-A", "--", ".", ":!.git-local"]);
|
|
1683
|
+
await o.wtGit.raw(["commit", "-m", commitMsg]);
|
|
1684
|
+
validation = await validateApply(o.wtGit, headBefore);
|
|
1685
|
+
if (validation.valid) {
|
|
1686
|
+
cb.onLog(`Rescue commit succeeded`, "green");
|
|
1687
|
+
}
|
|
1688
|
+
} catch (e) {
|
|
1689
|
+
cb.onLog(`Rescue commit failed: ${e.message}`, "red");
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1575
1693
|
if (!validation.valid) {
|
|
1576
1694
|
cb.onLog(`Validation failed: ${validation.errors.join("; ")}`, "red");
|
|
1577
1695
|
if (validation.dirtyFiles.length > 0) {
|
|
@@ -1607,7 +1725,7 @@ async function processOneCommit(o) {
|
|
|
1607
1725
|
};
|
|
1608
1726
|
writeReviewPacket(audit, packet, attempt);
|
|
1609
1727
|
const headBeforeReview = await getHead(o.wtGit);
|
|
1610
|
-
reviewResult = await reviewAppliedDiff(o.wtPath, packet);
|
|
1728
|
+
reviewResult = await reviewAppliedDiff(o.wtPath, packet, o.cb.onProgress);
|
|
1611
1729
|
audit.writeReviewResult(commit.hash, attempt, reviewResult);
|
|
1612
1730
|
const mutation = await verifyReviewIntegrity(o.wtGit, headBeforeReview);
|
|
1613
1731
|
if (mutation) {
|
|
@@ -1625,10 +1743,10 @@ async function processOneCommit(o) {
|
|
|
1625
1743
|
}
|
|
1626
1744
|
if (!reviewResult.approved) {
|
|
1627
1745
|
cb.onLog(`Review rejected: ${reviewResult.issues.join("; ")}`, "red");
|
|
1628
|
-
const maxFixRounds =
|
|
1746
|
+
const maxFixRounds = Infinity;
|
|
1629
1747
|
let fixed = false;
|
|
1630
1748
|
for (let fixRound = 1;fixRound <= maxFixRounds; fixRound++) {
|
|
1631
|
-
cb.onStatus(`Codex fixing review issues (round ${fixRound}
|
|
1749
|
+
cb.onStatus(`Codex fixing review issues (round ${fixRound})...`);
|
|
1632
1750
|
try {
|
|
1633
1751
|
await fixFromReview({
|
|
1634
1752
|
worktreePath: o.wtPath,
|
|
@@ -1659,7 +1777,7 @@ async function processOneCommit(o) {
|
|
|
1659
1777
|
appliedDiffStat: fixedStat
|
|
1660
1778
|
};
|
|
1661
1779
|
const headBeforeReReview = await getHead(o.wtGit);
|
|
1662
|
-
reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket);
|
|
1780
|
+
reviewResult = await reviewAppliedDiff(o.wtPath, fixPacket, o.cb.onProgress);
|
|
1663
1781
|
audit.writeReviewResult(commit.hash, attempt, {
|
|
1664
1782
|
...reviewResult,
|
|
1665
1783
|
fixRound
|
|
@@ -1690,7 +1808,7 @@ async function processOneCommit(o) {
|
|
|
1690
1808
|
kind: "failed",
|
|
1691
1809
|
commitHash: commit.hash,
|
|
1692
1810
|
commitMessage: commit.message,
|
|
1693
|
-
reason: `Review rejected after
|
|
1811
|
+
reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
|
|
1694
1812
|
failedPhase: "review",
|
|
1695
1813
|
reviewApproved: false,
|
|
1696
1814
|
reviewIssues: reviewResult.issues,
|
|
@@ -1704,7 +1822,7 @@ async function processOneCommit(o) {
|
|
|
1704
1822
|
kind: "failed",
|
|
1705
1823
|
commitHash: commit.hash,
|
|
1706
1824
|
commitMessage: commit.message,
|
|
1707
|
-
reason: `Review rejected after
|
|
1825
|
+
reason: `Review rejected after fix attempts: ${reviewResult.issues.join("; ")}`,
|
|
1708
1826
|
failedPhase: "review",
|
|
1709
1827
|
reviewApproved: false,
|
|
1710
1828
|
reviewIssues: reviewResult.issues,
|
|
@@ -1854,6 +1972,12 @@ async function runBatch(opts) {
|
|
|
1854
1972
|
log(` ${divider()}`);
|
|
1855
1973
|
log("");
|
|
1856
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
|
+
},
|
|
1857
1981
|
onLog(msg, color) {
|
|
1858
1982
|
if (jsonl)
|
|
1859
1983
|
emitJsonl({ event: "log", msg, color });
|
|
@@ -2883,7 +3007,7 @@ Behavior:
|
|
|
2883
3007
|
--no-fetch Skip fetch (default)
|
|
2884
3008
|
--no-review Skip Claude review pass
|
|
2885
3009
|
--no-auto-skip Don't auto-skip commits AI identifies as present
|
|
2886
|
-
--max-attempts <n> Max retries per commit (default:
|
|
3010
|
+
--max-attempts <n> Max retries per commit (default: unlimited)
|
|
2887
3011
|
--no-resume Don't resume from interrupted runs (default: auto-resume)
|
|
2888
3012
|
--config <path> Path to config file (default: auto-discover)
|
|
2889
3013
|
--help, -h Show this help
|