@humanbased/crosscheck 1.3.1-beta.5 → 1.5.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/README.md +16 -1
- package/assets/demo-block-to-approve.gif +0 -0
- package/assets/demo-block-to-approve.mp4 +0 -0
- package/dist/__tests__/base-ref-recovery.test.d.ts +2 -0
- package/dist/__tests__/base-ref-recovery.test.d.ts.map +1 -0
- package/dist/__tests__/base-ref-recovery.test.js +102 -0
- package/dist/__tests__/base-ref-recovery.test.js.map +1 -0
- package/dist/__tests__/codex-env.test.js +4 -0
- package/dist/__tests__/codex-env.test.js.map +1 -1
- package/dist/__tests__/credential-free-origin.test.js +134 -2
- package/dist/__tests__/credential-free-origin.test.js.map +1 -1
- package/dist/__tests__/demo-arc.test.d.ts +2 -0
- package/dist/__tests__/demo-arc.test.d.ts.map +1 -0
- package/dist/__tests__/demo-arc.test.js +73 -0
- package/dist/__tests__/demo-arc.test.js.map +1 -0
- package/dist/__tests__/doc-only-verdict.test.d.ts +2 -0
- package/dist/__tests__/doc-only-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/doc-only-verdict.test.js +79 -0
- package/dist/__tests__/doc-only-verdict.test.js.map +1 -0
- package/dist/__tests__/error-classification.test.js +14 -0
- package/dist/__tests__/error-classification.test.js.map +1 -1
- package/dist/__tests__/fix-requires-recheck.test.d.ts +2 -0
- package/dist/__tests__/fix-requires-recheck.test.d.ts.map +1 -0
- package/dist/__tests__/fix-requires-recheck.test.js +56 -0
- package/dist/__tests__/fix-requires-recheck.test.js.map +1 -0
- package/dist/__tests__/human-feedback.test.d.ts +2 -0
- package/dist/__tests__/human-feedback.test.d.ts.map +1 -0
- package/dist/__tests__/human-feedback.test.js +132 -0
- package/dist/__tests__/human-feedback.test.js.map +1 -0
- package/dist/__tests__/inconclusive-review.test.d.ts +2 -0
- package/dist/__tests__/inconclusive-review.test.d.ts.map +1 -0
- package/dist/__tests__/inconclusive-review.test.js +110 -0
- package/dist/__tests__/inconclusive-review.test.js.map +1 -0
- package/dist/__tests__/merge-gate.test.d.ts +2 -0
- package/dist/__tests__/merge-gate.test.d.ts.map +1 -0
- package/dist/__tests__/merge-gate.test.js +220 -0
- package/dist/__tests__/merge-gate.test.js.map +1 -0
- package/dist/__tests__/no-verdict.test.d.ts +2 -0
- package/dist/__tests__/no-verdict.test.d.ts.map +1 -0
- package/dist/__tests__/no-verdict.test.js +415 -0
- package/dist/__tests__/no-verdict.test.js.map +1 -0
- package/dist/__tests__/pr-spec.test.js +37 -0
- package/dist/__tests__/pr-spec.test.js.map +1 -1
- package/dist/__tests__/run.test.js +50 -1
- package/dist/__tests__/run.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +82 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/__tests__/standing-verdict-history.test.d.ts +2 -0
- package/dist/__tests__/standing-verdict-history.test.d.ts.map +1 -0
- package/dist/__tests__/standing-verdict-history.test.js +109 -0
- package/dist/__tests__/standing-verdict-history.test.js.map +1 -0
- package/dist/__tests__/step-plan.test.d.ts +2 -0
- package/dist/__tests__/step-plan.test.d.ts.map +1 -0
- package/dist/__tests__/step-plan.test.js +129 -0
- package/dist/__tests__/step-plan.test.js.map +1 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/merge.d.ts +11 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +188 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +29 -7
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts +53 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +197 -32
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +14 -3
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +8 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/review-strategy.json +5 -5
- package/dist/github/client.d.ts +18 -1
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +19 -3
- package/dist/github/client.js.map +1 -1
- package/dist/github/merge.d.ts +28 -0
- package/dist/github/merge.d.ts.map +1 -1
- package/dist/github/merge.js +41 -0
- package/dist/github/merge.js.map +1 -1
- package/dist/github/webhook.d.ts +1 -0
- package/dist/github/webhook.d.ts.map +1 -1
- package/dist/github/webhook.js.map +1 -1
- package/dist/lib/board.d.ts +1 -0
- package/dist/lib/board.d.ts.map +1 -1
- package/dist/lib/board.js +1 -1
- package/dist/lib/board.js.map +1 -1
- package/dist/lib/clone.d.ts +65 -1
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +252 -16
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/human-feedback.d.ts +9 -0
- package/dist/lib/human-feedback.d.ts.map +1 -0
- package/dist/lib/human-feedback.js +83 -0
- package/dist/lib/human-feedback.js.map +1 -0
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +6 -0
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/merge-gate.d.ts +70 -0
- package/dist/lib/merge-gate.d.ts.map +1 -0
- package/dist/lib/merge-gate.js +138 -0
- package/dist/lib/merge-gate.js.map +1 -0
- package/dist/lib/no-verdict.d.ts +104 -0
- package/dist/lib/no-verdict.d.ts.map +1 -0
- package/dist/lib/no-verdict.js +262 -0
- package/dist/lib/no-verdict.js.map +1 -0
- package/dist/lib/pr-spec.d.ts +7 -0
- package/dist/lib/pr-spec.d.ts.map +1 -1
- package/dist/lib/pr-spec.js +12 -1
- package/dist/lib/pr-spec.js.map +1 -1
- package/dist/lib/pr-workflow-state.d.ts +37 -0
- package/dist/lib/pr-workflow-state.d.ts.map +1 -1
- package/dist/lib/pr-workflow-state.js +58 -1
- package/dist/lib/pr-workflow-state.js.map +1 -1
- package/dist/lib/review-strategy.d.ts +14 -0
- package/dist/lib/review-strategy.d.ts.map +1 -1
- package/dist/lib/review-strategy.js +32 -1
- package/dist/lib/review-strategy.js.map +1 -1
- package/dist/lib/runner.d.ts +51 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +211 -105
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/verdict.d.ts +30 -1
- package/dist/lib/verdict.d.ts.map +1 -1
- package/dist/lib/verdict.js +94 -9
- package/dist/lib/verdict.js.map +1 -1
- package/dist/reviewers/codex-env.d.ts.map +1 -1
- package/dist/reviewers/codex-env.js +2 -0
- package/dist/reviewers/codex-env.js.map +1 -1
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +3 -1
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/fix.d.ts +2 -2
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +10 -4
- package/dist/reviewers/fix.js.map +1 -1
- package/docs/growth/launch/README.md +59 -0
- package/docs/growth/launch/essay-stop-letting-one-agent-review-its-own-code.md +118 -0
- package/docs/growth/launch/posts.md +175 -0
- package/docs/trust.md +16 -1
- package/package.json +11 -3
package/dist/lib/runner.js
CHANGED
|
@@ -9,8 +9,9 @@ import { runCodexReview } from '../reviewers/codex.js';
|
|
|
9
9
|
import { runClaudeReview } from '../reviewers/claude.js';
|
|
10
10
|
import { runFixStep, runCodexFixStep } from '../reviewers/fix.js';
|
|
11
11
|
import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
|
|
12
|
-
import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE } from '../lib/verdict.js';
|
|
12
|
+
import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE, DOC_ONLY_GATE_NOTE, detectInconclusiveReview, reviewHasNoFindings } from '../lib/verdict.js';
|
|
13
13
|
import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
|
|
14
|
+
import { fetchCommentsAfter, selectHumanFeedback, formatHumanFeedback } from '../lib/human-feedback.js';
|
|
14
15
|
import { autoFixBranchName, autoFixPRIntro, sourcePRHasMerged } from '../github/superseded-fix-pr.js';
|
|
15
16
|
import { verifyReviewedSha, isVerifiedReviewedSha, reviewedShaRejection } from '../github/reviewed-sha.js';
|
|
16
17
|
import { resolveLinearAuth, withWorker } from '../linear/identity.js';
|
|
@@ -19,9 +20,10 @@ import { shouldPostToLinear } from '../linear/comment.js';
|
|
|
19
20
|
import { getLinearCredentials } from '../config/loader.js';
|
|
20
21
|
import { acquireRemoteLock, releaseRemoteLock } from '../github/review-status.js';
|
|
21
22
|
import { log as fileLog, logError, classifyError } from '../lib/logger.js';
|
|
22
|
-
import { buildCommitTrailers } from '../lib/annotation.js';
|
|
23
|
+
import { buildCommitTrailers, parseAnnotation } from '../lib/annotation.js';
|
|
24
|
+
import { shaCovers } from '../lib/pr-workflow-state.js';
|
|
23
25
|
import { resolveClaudeModel, resolveCodexModel } from '../lib/review-models.js';
|
|
24
|
-
import { resolveReviewStrategy, escalate, clampToLevels } from './review-strategy.js';
|
|
26
|
+
import { resolveReviewStrategy, escalate, clampToLevels, isDocOnlyChange } from './review-strategy.js';
|
|
25
27
|
import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS } from '../config/schema.js';
|
|
26
28
|
import { buildStepIdentityFields } from '../lib/event-fields.js';
|
|
27
29
|
import { planAutoFixDelivery, forceWithLeaseArgs, parseLsRemoteOid, isLeaseRejection, assessFixBranchOwnership, isInvalidBaseError } from '../lib/auto-fix-branch.js';
|
|
@@ -33,6 +35,7 @@ import { tierTimeoutMs } from '../reviewers/tier-timeouts.js';
|
|
|
33
35
|
import { loadSkillCatalog } from '../skills/catalog.js';
|
|
34
36
|
import { createSkillActivationSession } from '../skills/broker.js';
|
|
35
37
|
import { formatSkillAttribution } from '../skills/attribution.js';
|
|
38
|
+
import { CompromisedCloneError, runGitWithoutHooks, changedFilesVsBase } from './clone.js';
|
|
36
39
|
const MAX_CROSSCHECK_COMMITS = 5;
|
|
37
40
|
const FIX_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
38
41
|
const REVIEW_RETRY_DELAY_MS = 2 * 60 * 1000;
|
|
@@ -46,6 +49,8 @@ function vendorTimeoutMs(timeoutSec) {
|
|
|
46
49
|
// Auth and quota/credit failures are operator/vendor-capacity issues that won't
|
|
47
50
|
// self-heal through an immediate retry. Transient subprocess failures can retry.
|
|
48
51
|
export function isRetryableFixError(err) {
|
|
52
|
+
if (err instanceof CompromisedCloneError)
|
|
53
|
+
return false;
|
|
49
54
|
const msg = err instanceof Error ? err.message : String(err);
|
|
50
55
|
return !/auth failure|not logged in|claude auth/i.test(msg) && !isSubscriptionLimitError(err);
|
|
51
56
|
}
|
|
@@ -149,18 +154,52 @@ export function resolveFixLanding(deliveryMode) {
|
|
|
149
154
|
case 'pull_request': return 'branch-then-separate-pr';
|
|
150
155
|
}
|
|
151
156
|
}
|
|
157
|
+
export function planStepsForClass(input) {
|
|
158
|
+
const { configuredSteps, classSteps, stepsExplicitlyScoped } = input;
|
|
159
|
+
const none = { steps: configuredSteps, outcome: 'unchanged', classTypes: [], dropped: 0, wouldDrop: 0 };
|
|
160
|
+
if (!classSteps || classSteps.length === 0)
|
|
161
|
+
return none;
|
|
162
|
+
// Only the three depth-ladder types narrow. conflict-resolve is orthogonal to
|
|
163
|
+
// depth and is handled by filterStepsByTypes, which keeps it whenever the depth
|
|
164
|
+
// permits code modification — so it must not be treated as a class step here.
|
|
165
|
+
const classTypes = classSteps.filter((t) => t === 'review' || t === 'fix' || t === 'recheck');
|
|
166
|
+
if (classTypes.length === 0)
|
|
167
|
+
return none;
|
|
168
|
+
const narrowed = filterStepsByTypes(configuredSteps, classTypes);
|
|
169
|
+
const delta = configuredSteps.length - narrowed.length;
|
|
170
|
+
if (stepsExplicitlyScoped) {
|
|
171
|
+
return { steps: configuredSteps, outcome: delta > 0 ? 'bypassed' : 'unchanged', classTypes, dropped: 0, wouldDrop: delta };
|
|
172
|
+
}
|
|
173
|
+
return { steps: narrowed, outcome: delta > 0 ? 'narrowed' : 'unchanged', classTypes, dropped: delta, wouldDrop: 0 };
|
|
174
|
+
}
|
|
152
175
|
// stepsRun holds every step the runner dispatched, skips included; results holds
|
|
153
176
|
// what each one did. Split them so callers can report a run where nothing
|
|
154
177
|
// happened without re-deriving the reasons from the log file.
|
|
155
178
|
export function summariseStepOutcomes(stepsRun, results) {
|
|
156
179
|
const outcomes = { ran: [], skipped: [] };
|
|
180
|
+
const detail = {};
|
|
157
181
|
for (const name of new Set(stepsRun)) {
|
|
158
182
|
const result = results[name];
|
|
159
|
-
if (result?.skipped)
|
|
183
|
+
if (result?.skipped) {
|
|
160
184
|
outcomes.skipped.push({ step: name, reason: result.skipReason ?? 'unknown' });
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
outcomes.ran.push(name);
|
|
188
|
+
if (!result)
|
|
189
|
+
continue;
|
|
190
|
+
const entry = {
|
|
191
|
+
...(result.vendor !== undefined && { vendor: result.vendor }),
|
|
192
|
+
...(result.tokens_used !== undefined && { tokensUsed: result.tokens_used }),
|
|
193
|
+
...(result.applied_count !== undefined && { appliedCount: result.applied_count }),
|
|
194
|
+
// Spread on key presence, not on truthiness: `verdict: null` is the whole
|
|
195
|
+
// point of recording this, and a falsiness check would drop it.
|
|
196
|
+
...('verdict' in result && { verdict: result.verdict ?? null }),
|
|
197
|
+
};
|
|
198
|
+
if (Object.keys(entry).length > 0)
|
|
199
|
+
detail[name] = entry;
|
|
163
200
|
}
|
|
201
|
+
if (Object.keys(detail).length > 0)
|
|
202
|
+
outcomes.ranDetail = detail;
|
|
164
203
|
return outcomes;
|
|
165
204
|
}
|
|
166
205
|
// Accumulates outcomes across the fix→recheck rounds of one invocation, so the
|
|
@@ -186,7 +225,21 @@ export function mergeStepOutcomes(base, next) {
|
|
|
186
225
|
if (!ranSet.has(entry.step))
|
|
187
226
|
skipped.set(entry.step, entry.reason);
|
|
188
227
|
}
|
|
189
|
-
|
|
228
|
+
// A step observed again in the later round is re-described by it, wholesale:
|
|
229
|
+
// a review that produced no verdict in round 1 and APPROVE in round 2 must not
|
|
230
|
+
// keep round 1's `verdict: null`, or the report calls a judged run unjudged.
|
|
231
|
+
const nextRan = new Set(next.ran);
|
|
232
|
+
const ranDetail = {};
|
|
233
|
+
for (const [step, detail] of Object.entries(base.ranDetail ?? {})) {
|
|
234
|
+
if (!nextRan.has(step))
|
|
235
|
+
ranDetail[step] = detail;
|
|
236
|
+
}
|
|
237
|
+
Object.assign(ranDetail, next.ranDetail ?? {});
|
|
238
|
+
return {
|
|
239
|
+
ran,
|
|
240
|
+
skipped: [...skipped].map(([step, reason]) => ({ step, reason })),
|
|
241
|
+
...(Object.keys(ranDetail).length > 0 && { ranDetail }),
|
|
242
|
+
};
|
|
190
243
|
}
|
|
191
244
|
function countComments(reviewText) {
|
|
192
245
|
const bullets = (reviewText.match(/^[-*•]\s/gm) ?? []).length;
|
|
@@ -352,40 +405,18 @@ function diffBucket(totalLines) {
|
|
|
352
405
|
*/
|
|
353
406
|
export function buildPRContext(ctx) {
|
|
354
407
|
const { tmpDir, pr } = ctx;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// backticks, and this value drives routing rather than best-effort logging.
|
|
358
|
-
const raw = execFileSync('git', ['diff', '--numstat', `origin/${pr.base.ref}...HEAD`], { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
359
|
-
if (!raw)
|
|
360
|
-
return null;
|
|
361
|
-
const files = [];
|
|
362
|
-
let additions = 0;
|
|
363
|
-
let deletions = 0;
|
|
364
|
-
for (const line of raw.split('\n')) {
|
|
365
|
-
// numstat: <added>\t<deleted>\t<path>. Binary files report '-' for both.
|
|
366
|
-
const [add, del, ...rest] = line.split('\t');
|
|
367
|
-
const path = rest.join('\t').trim();
|
|
368
|
-
if (!path)
|
|
369
|
-
continue;
|
|
370
|
-
files.push(path);
|
|
371
|
-
additions += parseInt(add, 10) || 0;
|
|
372
|
-
deletions += parseInt(del, 10) || 0;
|
|
373
|
-
}
|
|
374
|
-
if (files.length === 0)
|
|
375
|
-
return null;
|
|
376
|
-
return {
|
|
377
|
-
files,
|
|
378
|
-
additions,
|
|
379
|
-
deletions,
|
|
380
|
-
labels: pr.labels?.map(l => l.name) ?? [],
|
|
381
|
-
title: pr.title,
|
|
382
|
-
baseRef: pr.base.ref,
|
|
383
|
-
...(pr.base.repo.default_branch !== undefined && { defaultBranch: pr.base.repo.default_branch }),
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
408
|
+
const changed = changedFilesVsBase(tmpDir, pr.base.ref);
|
|
409
|
+
if (!changed)
|
|
387
410
|
return null;
|
|
388
|
-
|
|
411
|
+
return {
|
|
412
|
+
files: changed.files,
|
|
413
|
+
additions: changed.additions,
|
|
414
|
+
deletions: changed.deletions,
|
|
415
|
+
labels: pr.labels?.map(l => l.name) ?? [],
|
|
416
|
+
title: pr.title,
|
|
417
|
+
baseRef: pr.base.ref,
|
|
418
|
+
...(pr.base.repo.default_branch !== undefined && { defaultBranch: pr.base.repo.default_branch }),
|
|
419
|
+
};
|
|
389
420
|
}
|
|
390
421
|
/**
|
|
391
422
|
* Folds a resolved strategy into the quality config the reviewers receive, so
|
|
@@ -558,7 +589,7 @@ export async function pushWithNonFastForwardHandling(params) {
|
|
|
558
589
|
const { tmpDir, branch, token, log, fileLog, owner, repoName, prNumber } = params;
|
|
559
590
|
const env = { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token };
|
|
560
591
|
try {
|
|
561
|
-
|
|
592
|
+
runGitWithoutHooks(tmpDir, ['push', 'origin', `HEAD:${branch}`], env);
|
|
562
593
|
}
|
|
563
594
|
catch (pushErr) {
|
|
564
595
|
const pushMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
@@ -576,7 +607,7 @@ export async function pushWithNonFastForwardHandling(params) {
|
|
|
576
607
|
const fixCommitSha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: tmpDir, env, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
577
608
|
try {
|
|
578
609
|
// Fetch the latest state of the branch
|
|
579
|
-
|
|
610
|
+
runGitWithoutHooks(tmpDir, ['fetch', 'origin', branch], env);
|
|
580
611
|
try {
|
|
581
612
|
// Rebase our changes onto the latest branch state. Rebase onto FETCH_HEAD,
|
|
582
613
|
// NOT `origin/${branch}`: crosscheck's clone fetches the PR head without a
|
|
@@ -585,18 +616,18 @@ export async function pushWithNonFastForwardHandling(params) {
|
|
|
585
616
|
// land-on-branch push falls back to opening a separate fix PR. The fetch on
|
|
586
617
|
// the line above sets FETCH_HEAD to exactly the tip we just pulled, so that
|
|
587
618
|
// is the correct (and always-present) rebase target.
|
|
588
|
-
|
|
619
|
+
runGitWithoutHooks(tmpDir, ['rebase', 'FETCH_HEAD'], env);
|
|
589
620
|
}
|
|
590
621
|
catch (rebaseConflictErr) {
|
|
591
622
|
// If rebase fails (e.g., due to conflicts), abort it and restore the fix commit.
|
|
592
623
|
// Otherwise the outer catch's fallback logic sees a repo in in-progress-rebase
|
|
593
624
|
// state and incorrectly treats HEAD~1 as the fix commit.
|
|
594
|
-
|
|
595
|
-
|
|
625
|
+
runGitWithoutHooks(tmpDir, ['rebase', '--abort'], env);
|
|
626
|
+
runGitWithoutHooks(tmpDir, ['reset', '--hard', fixCommitSha], env);
|
|
596
627
|
throw rebaseConflictErr;
|
|
597
628
|
}
|
|
598
629
|
// Retry the push
|
|
599
|
-
|
|
630
|
+
runGitWithoutHooks(tmpDir, ['push', 'origin', `HEAD:${branch}`], env);
|
|
600
631
|
fileLog({
|
|
601
632
|
level: 'info',
|
|
602
633
|
event: 'push_rebase_succeeded',
|
|
@@ -643,6 +674,11 @@ export async function runWorkflow(ctx) {
|
|
|
643
674
|
// legitimately classify differently. Each comment cites the class that
|
|
644
675
|
// produced it, so the record stays accurate either way.
|
|
645
676
|
const strategy = resolveStrategyForPR(ctx);
|
|
677
|
+
// Read from the diff rather than the resolved class: `docs` matches a *fraction*
|
|
678
|
+
// of doc churn and exists only in smart mode, whereas the verdict cap needs the
|
|
679
|
+
// all-or-nothing answer on every install. Independent of `strategy` for that
|
|
680
|
+
// reason, and cheap — buildPRContext is one `git diff --numstat`.
|
|
681
|
+
const docOnlyChange = isDocOnlyChange(buildPRContext(ctx)?.files ?? []);
|
|
646
682
|
if (config.quality.mode === 'smart' && !strategy) {
|
|
647
683
|
// A smart-mode install quietly behaving as fixed is otherwise invisible.
|
|
648
684
|
fileLog({ level: 'warn', event: 'strategy_unresolved', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'pr_context_unavailable', fallback_tier: config.quality.tier });
|
|
@@ -667,25 +703,29 @@ export async function runWorkflow(ctx) {
|
|
|
667
703
|
}
|
|
668
704
|
// The class's step set NARROWS the configured pipeline; it never widens it.
|
|
669
705
|
// A repo set to review-only stays review-only whatever the class says, which
|
|
670
|
-
// matches how per-repo `crosscheck alter` overrides compose.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (strategy && strategy.tier === null) {
|
|
706
|
+
// matches how per-repo `crosscheck alter` overrides compose.
|
|
707
|
+
const stepPlan = planStepsForClass({
|
|
708
|
+
configuredSteps,
|
|
709
|
+
classSteps: strategy?.steps,
|
|
710
|
+
stepsExplicitlyScoped: ctx.stepsExplicitlyScoped === true,
|
|
711
|
+
});
|
|
712
|
+
if (stepPlan.outcome === 'bypassed') {
|
|
713
|
+
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} would drop ${stepPlan.wouldDrop} step${stepPlan.wouldDrop === 1 ? '' : 's'} — honouring --steps as given`));
|
|
714
|
+
fileLog({ level: 'info', event: 'strategy_narrowing_bypassed', repo: `${owner}/${repoName}`, pr: prNumber, pr_class: strategy.classId, requested: configuredSteps.map((x) => x.type), class_steps: stepPlan.classTypes, strategy_version: strategy.version });
|
|
715
|
+
}
|
|
716
|
+
else if (stepPlan.outcome === 'narrowed') {
|
|
717
|
+
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → ${stepPlan.classTypes.join(', ')} (${stepPlan.dropped} step${stepPlan.dropped === 1 ? '' : 's'} dropped)`));
|
|
718
|
+
fileLog({ level: 'info', event: 'strategy_steps_narrowed', repo: `${owner}/${repoName}`, pr: prNumber, pr_class: strategy.classId, configured: configuredSteps.map((x) => x.type), applied: stepPlan.steps.map((x) => x.type), strategy_version: strategy.version });
|
|
719
|
+
}
|
|
720
|
+
const steps = stepPlan.steps;
|
|
721
|
+
// A null tier means the class skips this PR outright. An explicit `--steps`
|
|
722
|
+
// overrides that for the same reason it overrides narrowing — but say so, or the
|
|
723
|
+
// run looks like the class simply did not match.
|
|
724
|
+
if (strategy && strategy.tier === null && ctx.stepsExplicitlyScoped) {
|
|
725
|
+
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} would skip this PR (${strategy.reason}) — honouring --steps as given`));
|
|
726
|
+
fileLog({ level: 'info', event: 'strategy_class_skip_bypassed', repo: `${owner}/${repoName}`, pr: prNumber, pr_class: strategy.classId, strategy_version: strategy.version });
|
|
727
|
+
}
|
|
728
|
+
if (strategy && strategy.tier === null && !ctx.stepsExplicitlyScoped) {
|
|
689
729
|
log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → skipped (${strategy.reason})`));
|
|
690
730
|
fileLog({ level: 'info', event: 'pr_skipped', repo: `${owner}/${repoName}`, pr: prNumber, reason: 'strategy_class_skip', pr_class: strategy.classId, strategy_version: strategy.version });
|
|
691
731
|
return { verdict: null, strategySkipped: strategy.classId };
|
|
@@ -871,18 +911,40 @@ export async function runWorkflow(ctx) {
|
|
|
871
911
|
let effort;
|
|
872
912
|
let retried;
|
|
873
913
|
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
914
|
+
// Hoisted up from the post-review linking code below (which still reuses
|
|
915
|
+
// this value): a recheck needs to know which review it is rechecking
|
|
916
|
+
// before it runs, not just when linking the posted comment afterward, so
|
|
917
|
+
// it can pull in anything the PR author (or their agent) said in reply.
|
|
918
|
+
let priorReviewId;
|
|
919
|
+
if (isRecheck) {
|
|
920
|
+
priorReviewId = Object.values(results).reverse().find(r => r.commentId !== undefined)?.commentId;
|
|
921
|
+
if (priorReviewId === undefined) {
|
|
922
|
+
priorReviewId = await getLastCrossCheckCommentId(owner, repoName, prNumber, token);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
let humanFeedbackBlock = '';
|
|
926
|
+
if (priorReviewId !== undefined) {
|
|
927
|
+
try {
|
|
928
|
+
const commentsAfter = await fetchCommentsAfter(owner, repoName, prNumber, token, priorReviewId);
|
|
929
|
+
humanFeedbackBlock = formatHumanFeedback(selectHumanFeedback(commentsAfter, pr.user.login));
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
fileLog({ level: 'warn', event: 'human_feedback_fetch_failed', repo: `${owner}/${repoName}`, pr: prNumber, error: err instanceof Error ? err.message : String(err) });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const reviewContext = [ctx.issueContext, humanFeedbackBlock].filter(Boolean).join('\n\n') || undefined;
|
|
874
936
|
// Under `quality.mode: smart` the PR's class picks the tier; under fixed
|
|
875
937
|
// this is config.quality untouched.
|
|
876
938
|
const runReviewWithVendor = async (candidate) => {
|
|
877
939
|
if (candidate === 'codex') {
|
|
878
940
|
;
|
|
879
|
-
({ review: rawReview, tokensUsed, model, effort, retried } = await runCodexReview(tmpDir, pr.base.ref, pr.title, quality, codexVendor, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec), log,
|
|
941
|
+
({ review: rawReview, tokensUsed, model, effort, retried } = await runCodexReview(tmpDir, pr.base.ref, pr.title, quality, codexVendor, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec), log, reviewContext, skillSession, config.skills.codex_full_access));
|
|
880
942
|
inputTokens = undefined;
|
|
881
943
|
outputTokens = undefined;
|
|
882
944
|
}
|
|
883
945
|
else {
|
|
884
946
|
;
|
|
885
|
-
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, effort, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, quality, claudeVendor, config.budget.per_review_usd, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), !!ctx.roundMode, log,
|
|
947
|
+
({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, effort, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, quality, claudeVendor, config.budget.per_review_usd, step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), !!ctx.roundMode, log, reviewContext, skillSession));
|
|
886
948
|
}
|
|
887
949
|
};
|
|
888
950
|
try {
|
|
@@ -954,17 +1016,31 @@ export async function runWorkflow(ctx) {
|
|
|
954
1016
|
if (parsed.verdict === null) {
|
|
955
1017
|
fileLog({ level: 'warn', event: 'verdict_parse_failed', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, ...stepIdentity, output_length: rawReview.length });
|
|
956
1018
|
}
|
|
1019
|
+
// Inconclusive gate — must run BEFORE the severity gate. A reviewer that
|
|
1020
|
+
// reports it could not review produces a findings-free body, which the
|
|
1021
|
+
// severity gate reads as "nits only" and upgrades to APPROVE: a PR cleared
|
|
1022
|
+
// to merge on the strength of a review that never ran. Refuse to post at
|
|
1023
|
+
// all instead, the same way an unverifiable sha is refused below.
|
|
1024
|
+
const inconclusive = detectInconclusiveReview(clean);
|
|
1025
|
+
if (inconclusive.inconclusive) {
|
|
1026
|
+
fileLog({ level: 'error', event: 'review_inconclusive', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, model, ...stepIdentity, reason: inconclusive.reason, raw_verdict: parsed.verdict, output_length: rawReview.length, ...(ctx.round !== undefined && { round: ctx.round }), ...triggerField });
|
|
1027
|
+
log(chalk.red(`✗ ${reviewer} did not review PR #${prNumber} — ${inconclusive.reason}`));
|
|
1028
|
+
// The review cost vendor tokens — surface it rather than lose it silently.
|
|
1029
|
+
log(chalk.dim(`\n--- unposted review ---\n${clean}\n--- end ---`));
|
|
1030
|
+
throw new Error(`${reviewer} review inconclusive — ${inconclusive.reason}`);
|
|
1031
|
+
}
|
|
957
1032
|
// Severity gate: NEEDS WORK with only P3 nits is downgraded to APPROVE so
|
|
958
1033
|
// suggestion-only reviews don't drive the fix/recheck loop. P2 (correctness)
|
|
959
1034
|
// and above keep NEEDS WORK and require human attention before merge.
|
|
960
|
-
const gate = applySeverityGate(parsed.verdict, clean);
|
|
1035
|
+
const gate = applySeverityGate(parsed.verdict, clean, { docOnly: docOnlyChange });
|
|
961
1036
|
const verdict = gate.verdict;
|
|
962
1037
|
if (gate.downgraded) {
|
|
963
|
-
fileLog({ level: 'info', event: 'verdict_severity_gated', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, ...stepIdentity, raw_verdict: parsed.verdict, gated_verdict: verdict });
|
|
1038
|
+
fileLog({ level: 'info', event: 'verdict_severity_gated', repo: `${owner}/${repoName}`, pr: prNumber, reviewer, ...stepIdentity, raw_verdict: parsed.verdict, gated_verdict: verdict, reason: gate.reason });
|
|
964
1039
|
}
|
|
1040
|
+
const gateNote = gate.reason === 'doc_only' ? DOC_ONLY_GATE_NOTE : SEVERITY_GATE_NOTE;
|
|
965
1041
|
const baseBody = verdict === null
|
|
966
1042
|
? `${NULL_VERDICT_WARNING}\n\n${clean}`
|
|
967
|
-
: prependVerdictToComment(gate.downgraded ? `${
|
|
1043
|
+
: prependVerdictToComment(gate.downgraded ? `${gateNote}\n\n${clean}` : clean, verdict);
|
|
968
1044
|
// Skills are not folded into the body — postReviewComment renders the
|
|
969
1045
|
// receipt beneath the attribution footer.
|
|
970
1046
|
const commentBody = retried
|
|
@@ -989,16 +1065,9 @@ export async function runWorkflow(ctx) {
|
|
|
989
1065
|
else {
|
|
990
1066
|
onPhaseChange(isRecheck ? 'posting recheck...' : 'posting comment...', phaseUpdate);
|
|
991
1067
|
const octokit = createGithubClient(token);
|
|
992
|
-
//
|
|
993
|
-
//
|
|
994
|
-
//
|
|
995
|
-
let priorReviewId;
|
|
996
|
-
if (isRecheck) {
|
|
997
|
-
priorReviewId = Object.values(results).reverse().find(r => r.commentId !== undefined)?.commentId;
|
|
998
|
-
if (priorReviewId === undefined) {
|
|
999
|
-
priorReviewId = await getLastCrossCheckCommentId(owner, repoName, prNumber, token);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1068
|
+
// priorReviewId (for rechecks: the original review comment to link back
|
|
1069
|
+
// to) is already resolved above, before the review ran — reused here
|
|
1070
|
+
// rather than looked up a second time.
|
|
1002
1071
|
// Pre-compute next_step for the annotation so readers can skip full
|
|
1003
1072
|
// comment scans: find the first remaining step whose when-condition holds.
|
|
1004
1073
|
const currentStepIdx = steps.indexOf(step);
|
|
@@ -1135,6 +1204,37 @@ export async function runWorkflow(ctx) {
|
|
|
1135
1204
|
skipFix('no_review_comment');
|
|
1136
1205
|
continue;
|
|
1137
1206
|
}
|
|
1207
|
+
// A review with nothing actionable in it cannot produce a fix. Dispatching one
|
|
1208
|
+
// anyway spends a full vendor call to be told NO_CHANGES — the outcome that
|
|
1209
|
+
// made `ck fix` look like it had worked when its review was a non-review.
|
|
1210
|
+
if (reviewHasNoFindings(reviewCommentBody)) {
|
|
1211
|
+
log(chalk.dim(` review has no actionable findings — nothing for the fix step to apply`));
|
|
1212
|
+
skipFix('review_has_no_findings');
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
// Findings raised against a different commit still usually apply, so this
|
|
1216
|
+
// warns rather than skips — but silently fixing against a stale review is how
|
|
1217
|
+
// a round gets spent re-fixing something the author already pushed.
|
|
1218
|
+
// shaCovers, not ===: annotations carry the short or full form interchangeably,
|
|
1219
|
+
// and a raw compare would call every short-form annotation stale.
|
|
1220
|
+
const reviewSha = parseAnnotation(reviewCommentBody)?.sha;
|
|
1221
|
+
if (reviewSha && !shaCovers(reviewSha, pr.head.sha)) {
|
|
1222
|
+
log(chalk.yellow(`⚠ review was written against ${reviewSha.slice(0, 7)}, PR head is ${pr.head.sha.slice(0, 7)} — fixing against a stale review`));
|
|
1223
|
+
fileLog({ level: 'warn', event: 'fix_review_stale', repo: `${owner}/${repoName}`, pr: prNumber, review_sha: reviewSha, head_sha: pr.head.sha, ...triggerField });
|
|
1224
|
+
}
|
|
1225
|
+
// Replies the PR author (or their agent) posted since that review — e.g.
|
|
1226
|
+
// "already fixed in the last commit" or "not applicable here" — so the
|
|
1227
|
+
// fixer doesn't blindly redo work the thread already settled.
|
|
1228
|
+
let fixHumanFeedback = '';
|
|
1229
|
+
if (reviewCommentId !== undefined) {
|
|
1230
|
+
try {
|
|
1231
|
+
const commentsAfter = await fetchCommentsAfter(owner, repoName, prNumber, token, reviewCommentId);
|
|
1232
|
+
fixHumanFeedback = formatHumanFeedback(selectHumanFeedback(commentsAfter, pr.user.login));
|
|
1233
|
+
}
|
|
1234
|
+
catch (err) {
|
|
1235
|
+
fileLog({ level: 'warn', event: 'human_feedback_fetch_failed', repo: `${owner}/${repoName}`, pr: prNumber, error: err instanceof Error ? err.message : String(err) });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1138
1238
|
// Vendor is resolved from the workflow step's reviewer field, same as review/recheck steps.
|
|
1139
1239
|
// Use 'origin' to fix with the same vendor that authored the PR (recommended default).
|
|
1140
1240
|
// resolveFixVendor extends resolveReviewer with a human-origin fallback so human-authored
|
|
@@ -1185,17 +1285,19 @@ export async function runWorkflow(ctx) {
|
|
|
1185
1285
|
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
1186
1286
|
const runFix = async (v) => {
|
|
1187
1287
|
if (v === 'codex') {
|
|
1188
|
-
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs, skillSession, codexVendor.effort, config.skills.codex_full_access);
|
|
1288
|
+
return runCodexFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', codexFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.codex.timeout_sec) ?? tierMs, skillSession, codexVendor.effort, config.skills.codex_full_access, fixHumanFeedback);
|
|
1189
1289
|
}
|
|
1190
1290
|
// roundConfig, not config: runFixStep reads vendors.claude.effort and
|
|
1191
1291
|
// quality.tier out of it, and both must be this round's values.
|
|
1192
|
-
return runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', roundConfig, claudeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec) ?? tierMs, skillSession);
|
|
1292
|
+
return runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', roundConfig, claudeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec) ?? tierMs, skillSession, fixHumanFeedback);
|
|
1193
1293
|
};
|
|
1194
1294
|
try {
|
|
1195
1295
|
;
|
|
1196
1296
|
({ appliedCount, changedFiles: fixChangedFiles, tokensUsed: fixTokensUsed, effort: fixEffort } = await runFix(vendor));
|
|
1197
1297
|
}
|
|
1198
1298
|
catch (err) {
|
|
1299
|
+
if (err instanceof CompromisedCloneError)
|
|
1300
|
+
throw err;
|
|
1199
1301
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 1, vendor }, err);
|
|
1200
1302
|
const fallbackVendor = resolveLimitFallbackVendor(vendor, effectiveType, config);
|
|
1201
1303
|
if (isSubscriptionLimitError(err)) {
|
|
@@ -1211,6 +1313,8 @@ export async function runWorkflow(ctx) {
|
|
|
1211
1313
|
activeVendor = fallbackVendor;
|
|
1212
1314
|
}
|
|
1213
1315
|
catch (fallbackErr) {
|
|
1316
|
+
if (fallbackErr instanceof CompromisedCloneError)
|
|
1317
|
+
throw fallbackErr;
|
|
1214
1318
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 1, vendor: fallbackVendor }, fallbackErr);
|
|
1215
1319
|
activeVendor = fallbackVendor; // retry uses the fallback vendor since primary already failed
|
|
1216
1320
|
fixErr = fallbackErr;
|
|
@@ -1233,6 +1337,8 @@ export async function runWorkflow(ctx) {
|
|
|
1233
1337
|
fixErr = undefined;
|
|
1234
1338
|
}
|
|
1235
1339
|
catch (retryErr) {
|
|
1340
|
+
if (retryErr instanceof CompromisedCloneError)
|
|
1341
|
+
throw retryErr;
|
|
1236
1342
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'fix', attempt: 2 }, retryErr);
|
|
1237
1343
|
fixErr = retryErr;
|
|
1238
1344
|
}
|
|
@@ -1297,14 +1403,14 @@ export async function runWorkflow(ctx) {
|
|
|
1297
1403
|
const landing = resolveFixLanding(deliveryMode);
|
|
1298
1404
|
if (landing === 'branch') {
|
|
1299
1405
|
const fixModel = activeVendor === 'codex' ? codexFixModel : claudeFixModel;
|
|
1300
|
-
|
|
1301
|
-
|
|
1406
|
+
runGitWithoutHooks(tmpDir, ['add', '-A']);
|
|
1407
|
+
runGitWithoutHooks(tmpDir, [
|
|
1302
1408
|
'commit',
|
|
1303
1409
|
'-m',
|
|
1304
1410
|
fixCommitSubject(appliedCount, activeVendor),
|
|
1305
1411
|
'-m',
|
|
1306
1412
|
buildCommitTrailers({ reviewer: activeVendor, model: fixModel, step: 'fix', service: 'crosscheck' }),
|
|
1307
|
-
]
|
|
1413
|
+
]);
|
|
1308
1414
|
const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
1309
1415
|
await pushWithNonFastForwardHandling({
|
|
1310
1416
|
tmpDir,
|
|
@@ -1367,14 +1473,14 @@ export async function runWorkflow(ctx) {
|
|
|
1367
1473
|
// original PR. Only when that push can't land — e.g. the PR was merged and
|
|
1368
1474
|
// its branch deleted, or the branch is protected — do we fall back to opening
|
|
1369
1475
|
// a separate follow-up PR that carries the very same commit.
|
|
1370
|
-
|
|
1371
|
-
|
|
1476
|
+
runGitWithoutHooks(tmpDir, ['add', '-A']);
|
|
1477
|
+
runGitWithoutHooks(tmpDir, [
|
|
1372
1478
|
'commit',
|
|
1373
1479
|
'-m',
|
|
1374
1480
|
fixPRCommitSubject(prNumber, activeVendor),
|
|
1375
1481
|
'-m',
|
|
1376
1482
|
buildCommitTrailers({ reviewer: activeVendor, model: fixModel, step: 'fix', service: 'crosscheck' }),
|
|
1377
|
-
]
|
|
1483
|
+
]);
|
|
1378
1484
|
const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
1379
1485
|
let landedOnBranch = false;
|
|
1380
1486
|
try {
|
|
@@ -1445,7 +1551,7 @@ export async function runWorkflow(ctx) {
|
|
|
1445
1551
|
// either with a sha the repository cannot resolve — which verifyReviewedSha
|
|
1446
1552
|
// refuses to post (#290) — or with one no open PR carries.
|
|
1447
1553
|
const restorePRHeadInClone = () => {
|
|
1448
|
-
|
|
1554
|
+
runGitWithoutHooks(tmpDir, ['reset', '--hard', 'HEAD~1']);
|
|
1449
1555
|
};
|
|
1450
1556
|
// Deliver the fix as a diff on the original PR. Used wherever opening the
|
|
1451
1557
|
// follow-up PR would either create a stale artifact or overwrite a branch that
|
|
@@ -1559,7 +1665,7 @@ export async function runWorkflow(ctx) {
|
|
|
1559
1665
|
continue;
|
|
1560
1666
|
}
|
|
1561
1667
|
try {
|
|
1562
|
-
|
|
1668
|
+
runGitWithoutHooks(tmpDir, forceWithLeaseArgs(fixBranch, remoteOid), gitEnv);
|
|
1563
1669
|
}
|
|
1564
1670
|
catch (pushErr) {
|
|
1565
1671
|
const pushMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
|
|
@@ -1582,7 +1688,7 @@ export async function runWorkflow(ctx) {
|
|
|
1582
1688
|
if (remoteOid !== null)
|
|
1583
1689
|
return;
|
|
1584
1690
|
try {
|
|
1585
|
-
|
|
1691
|
+
runGitWithoutHooks(tmpDir, ['push', 'origin', '--delete', fixBranch], gitEnv);
|
|
1586
1692
|
fileLog({ level: 'info', event: 'fix_branch_rolled_back', repo: `${owner}/${repoName}`, pr: prNumber, branch: fixBranch, sha: newSha });
|
|
1587
1693
|
}
|
|
1588
1694
|
catch (deleteErr) {
|
|
@@ -1705,10 +1811,10 @@ export async function runWorkflow(ctx) {
|
|
|
1705
1811
|
// real conflict markers and UU entries that findConflictedFiles can detect.
|
|
1706
1812
|
let hasMergeConflicts = false;
|
|
1707
1813
|
try {
|
|
1708
|
-
|
|
1814
|
+
runGitWithoutHooks(tmpDir, ['merge', '--no-commit', `origin/${pr.base.ref}`]);
|
|
1709
1815
|
// Clean merge — undo the staged merge state and skip this step
|
|
1710
1816
|
try {
|
|
1711
|
-
|
|
1817
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1712
1818
|
}
|
|
1713
1819
|
catch { /* ignore */ }
|
|
1714
1820
|
}
|
|
@@ -1722,7 +1828,7 @@ export async function runWorkflow(ctx) {
|
|
|
1722
1828
|
const conflictedFiles = findConflictedFiles(tmpDir);
|
|
1723
1829
|
if (conflictedFiles.length === 0) {
|
|
1724
1830
|
try {
|
|
1725
|
-
|
|
1831
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1726
1832
|
}
|
|
1727
1833
|
catch { /* ignore */ }
|
|
1728
1834
|
skipConflictResolve('no_conflicts');
|
|
@@ -1740,7 +1846,7 @@ export async function runWorkflow(ctx) {
|
|
|
1740
1846
|
}
|
|
1741
1847
|
if (!vendor) {
|
|
1742
1848
|
try {
|
|
1743
|
-
|
|
1849
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1744
1850
|
}
|
|
1745
1851
|
catch { /* ignore */ }
|
|
1746
1852
|
;
|
|
@@ -1749,7 +1855,7 @@ export async function runWorkflow(ctx) {
|
|
|
1749
1855
|
}
|
|
1750
1856
|
if (vendor === 'codex') {
|
|
1751
1857
|
try {
|
|
1752
|
-
|
|
1858
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1753
1859
|
}
|
|
1754
1860
|
catch { /* ignore */ }
|
|
1755
1861
|
;
|
|
@@ -1762,7 +1868,7 @@ export async function runWorkflow(ctx) {
|
|
|
1762
1868
|
const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
|
|
1763
1869
|
if (isFork) {
|
|
1764
1870
|
try {
|
|
1765
|
-
|
|
1871
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1766
1872
|
}
|
|
1767
1873
|
catch { /* ignore */ }
|
|
1768
1874
|
;
|
|
@@ -1772,7 +1878,7 @@ export async function runWorkflow(ctx) {
|
|
|
1772
1878
|
const crCommitCount = countCrosscheckCommitsForPRDetailed(tmpDir, pr.base.ref);
|
|
1773
1879
|
if (crCommitCount.count >= MAX_CROSSCHECK_COMMITS) {
|
|
1774
1880
|
try {
|
|
1775
|
-
|
|
1881
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1776
1882
|
}
|
|
1777
1883
|
catch { /* ignore */ }
|
|
1778
1884
|
log(crCommitCount.scoped
|
|
@@ -1796,7 +1902,7 @@ export async function runWorkflow(ctx) {
|
|
|
1796
1902
|
catch (err) {
|
|
1797
1903
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
|
|
1798
1904
|
try {
|
|
1799
|
-
|
|
1905
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1800
1906
|
}
|
|
1801
1907
|
catch { /* ignore */ }
|
|
1802
1908
|
skipConflictResolve(isSubscriptionLimitError(err) ? 'vendor_limit' : 'resolve_error');
|
|
@@ -1809,7 +1915,7 @@ export async function runWorkflow(ctx) {
|
|
|
1809
1915
|
logSkillsNoneActivated(skillSession, { step_type: 'conflict-resolve', step_name: step.name });
|
|
1810
1916
|
if (appliedCount === 0) {
|
|
1811
1917
|
try {
|
|
1812
|
-
|
|
1918
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1813
1919
|
}
|
|
1814
1920
|
catch { /* ignore */ }
|
|
1815
1921
|
onPhaseChange('', { phase: 'fixed', fixCount: 0, fixTokens: resolveTokensUsed });
|
|
@@ -1836,7 +1942,7 @@ export async function runWorkflow(ctx) {
|
|
|
1836
1942
|
}
|
|
1837
1943
|
if (filesWithMarkers.length > 0) {
|
|
1838
1944
|
try {
|
|
1839
|
-
|
|
1945
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1840
1946
|
}
|
|
1841
1947
|
catch { /* ignore */ }
|
|
1842
1948
|
log(chalk.yellow(`⚠ PR #${prNumber}: ${filesWithMarkers.length} file(s) still contain conflict markers — skipping commit`));
|
|
@@ -1853,7 +1959,7 @@ export async function runWorkflow(ctx) {
|
|
|
1853
1959
|
// resolvedPaths is derived from model output and PR-controlled filenames.
|
|
1854
1960
|
for (const p of resolvedPaths) {
|
|
1855
1961
|
try {
|
|
1856
|
-
|
|
1962
|
+
runGitWithoutHooks(tmpDir, ['add', '--', p]);
|
|
1857
1963
|
}
|
|
1858
1964
|
catch { /* skip */ }
|
|
1859
1965
|
}
|
|
@@ -1868,7 +1974,7 @@ export async function runWorkflow(ctx) {
|
|
|
1868
1974
|
catch { /* ignore */ }
|
|
1869
1975
|
if (unmergedPaths.length > 0) {
|
|
1870
1976
|
try {
|
|
1871
|
-
|
|
1977
|
+
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1872
1978
|
}
|
|
1873
1979
|
catch { /* ignore */ }
|
|
1874
1980
|
log(chalk.yellow(`⚠ PR #${prNumber}: ${unmergedPaths.length} unmerged path(s) remain after resolve — skipping commit`));
|
|
@@ -1876,13 +1982,13 @@ export async function runWorkflow(ctx) {
|
|
|
1876
1982
|
skipConflictResolve('unmerged_paths');
|
|
1877
1983
|
continue;
|
|
1878
1984
|
}
|
|
1879
|
-
|
|
1985
|
+
runGitWithoutHooks(tmpDir, [
|
|
1880
1986
|
'commit',
|
|
1881
1987
|
'-m',
|
|
1882
1988
|
conflictResolveCommitSubject(conflictedFiles.length, vendor),
|
|
1883
1989
|
'-m',
|
|
1884
1990
|
buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel, step: 'conflict-resolve', service: 'crosscheck' }),
|
|
1885
|
-
]
|
|
1991
|
+
]);
|
|
1886
1992
|
const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
1887
1993
|
await pushWithNonFastForwardHandling({
|
|
1888
1994
|
tmpDir,
|