@humanbased/crosscheck 1.8.0-beta.17 → 1.8.0-beta.23
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/crosscheck.config.example.yml +23 -0
- package/dist/__tests__/conflict-resolve.test.js +93 -1
- package/dist/__tests__/conflict-resolve.test.js.map +1 -1
- package/dist/__tests__/onboard-preservation.test.js +15 -0
- package/dist/__tests__/onboard-preservation.test.js.map +1 -1
- package/dist/__tests__/opencode-auth.test.js +29 -1
- package/dist/__tests__/opencode-auth.test.js.map +1 -1
- package/dist/__tests__/opencode-cli-compat.test.d.ts +2 -0
- package/dist/__tests__/opencode-cli-compat.test.d.ts.map +1 -0
- package/dist/__tests__/opencode-cli-compat.test.js +108 -0
- package/dist/__tests__/opencode-cli-compat.test.js.map +1 -0
- package/dist/__tests__/opencode-policy.test.js +8 -3
- package/dist/__tests__/opencode-policy.test.js.map +1 -1
- package/dist/__tests__/opencode-standalone.test.js +42 -30
- package/dist/__tests__/opencode-standalone.test.js.map +1 -1
- package/dist/__tests__/optimize.test.js +1 -0
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +65 -21
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +3 -5
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/onboard.d.ts +3 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +92 -12
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/optimize.d.ts.map +1 -1
- package/dist/commands/optimize.js +3 -5
- package/dist/commands/optimize.js.map +1 -1
- package/dist/config/schema.d.ts +37 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/lib/comment-bodies.d.ts +3 -0
- package/dist/lib/comment-bodies.d.ts.map +1 -1
- package/dist/lib/comment-bodies.js +2 -3
- package/dist/lib/comment-bodies.js.map +1 -1
- package/dist/lib/review-memory.d.ts +2 -2
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +114 -66
- package/dist/lib/runner.js.map +1 -1
- package/dist/reviewers/conflict-resolve.d.ts +7 -3
- package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
- package/dist/reviewers/conflict-resolve.js +197 -11
- package/dist/reviewers/conflict-resolve.js.map +1 -1
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +8 -6
- package/dist/reviewers/fix.js.map +1 -1
- package/dist/reviewers/opencode.d.ts +25 -2
- package/dist/reviewers/opencode.d.ts.map +1 -1
- package/dist/reviewers/opencode.js +128 -47
- package/dist/reviewers/opencode.js.map +1 -1
- package/get-started.md +15 -1
- package/package.json +1 -1
package/dist/lib/runner.js
CHANGED
|
@@ -9,7 +9,7 @@ import { runCodexReview } from '../reviewers/codex.js';
|
|
|
9
9
|
import { runClaudeReview } from '../reviewers/claude.js';
|
|
10
10
|
import { runOpenCodeReview } from '../reviewers/opencode.js';
|
|
11
11
|
import { runFixStep, runCodexFixStep, runOpenCodeFixStep } from '../reviewers/fix.js';
|
|
12
|
-
import {
|
|
12
|
+
import { runConflictResolveForVendor, findConflictedFiles } from '../reviewers/conflict-resolve.js';
|
|
13
13
|
import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE, DOC_ONLY_GATE_NOTE, detectInconclusiveReview, reviewHasNoFindings } from '../lib/verdict.js';
|
|
14
14
|
import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
|
|
15
15
|
import { fetchCommentsAfter, selectHumanFeedback, formatHumanFeedback } from '../lib/human-feedback.js';
|
|
@@ -250,36 +250,85 @@ function countComments(reviewText) {
|
|
|
250
250
|
const numbered = (reviewText.match(/^\d+\.\s/gm) ?? []).length;
|
|
251
251
|
return bullets + numbered;
|
|
252
252
|
}
|
|
253
|
-
function
|
|
253
|
+
function priorityTaskForStep(stepType) {
|
|
254
|
+
if (stepType === 'conflict-resolve')
|
|
255
|
+
return 'conflict_resolve';
|
|
256
|
+
if (stepType === 'fix')
|
|
257
|
+
return 'fix';
|
|
258
|
+
return 'review';
|
|
259
|
+
}
|
|
260
|
+
function originVendorOf(origin) {
|
|
261
|
+
return origin === 'claude' || origin === 'codex' || origin === 'opencode' ? origin : null;
|
|
262
|
+
}
|
|
263
|
+
// Configured order first, then any other enabled/capable vendor so a newly
|
|
264
|
+
// enabled agent is still usable without re-running onboard. An empty configured
|
|
265
|
+
// list returns [] — callers then use the legacy codex-first order, which keeps
|
|
266
|
+
// pre-priority configs byte-for-byte behaviourally identical.
|
|
267
|
+
function priorityOrder(task, config, stepType) {
|
|
268
|
+
const configured = config.agent_priority?.[task] ?? [];
|
|
269
|
+
if (configured.length === 0)
|
|
270
|
+
return [];
|
|
271
|
+
const usable = (vendor) => config.vendors[vendor].enabled && supportsStep(vendor, stepType);
|
|
272
|
+
const ordered = configured.filter(usable);
|
|
273
|
+
for (const vendor of ['claude', 'codex', 'opencode']) {
|
|
274
|
+
if (usable(vendor) && !ordered.includes(vendor))
|
|
275
|
+
ordered.push(vendor);
|
|
276
|
+
}
|
|
277
|
+
return ordered;
|
|
278
|
+
}
|
|
279
|
+
function legacyAutoReviewer(origin, config) {
|
|
280
|
+
if (origin === 'claude' && config.vendors.codex.enabled)
|
|
281
|
+
return 'codex';
|
|
282
|
+
if (origin === 'claude' && config.vendors.opencode.enabled)
|
|
283
|
+
return 'opencode';
|
|
284
|
+
if (origin === 'codex' && config.vendors.claude.enabled)
|
|
285
|
+
return 'claude';
|
|
286
|
+
if (origin === 'codex' && config.vendors.opencode.enabled)
|
|
287
|
+
return 'opencode';
|
|
288
|
+
if (origin === 'opencode' && config.vendors.claude.enabled)
|
|
289
|
+
return 'claude';
|
|
290
|
+
if (origin === 'opencode' && config.vendors.codex.enabled)
|
|
291
|
+
return 'codex';
|
|
292
|
+
if (config.vendors.codex.enabled)
|
|
293
|
+
return 'codex';
|
|
294
|
+
if (config.vendors.claude.enabled)
|
|
295
|
+
return 'claude';
|
|
296
|
+
if (config.vendors.opencode.enabled)
|
|
297
|
+
return 'opencode';
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
function resolveReviewer(reviewer, origin, config, fallback, stepType = 'review') {
|
|
301
|
+
const originVendor = originVendorOf(origin);
|
|
302
|
+
const task = priorityTaskForStep(stepType);
|
|
303
|
+
const writesCode = stepType === 'fix' || stepType === 'conflict-resolve';
|
|
254
304
|
if (reviewer === 'origin') {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
305
|
+
// Fix and conflict-resolve deliberately start from the PR author's own
|
|
306
|
+
// agent — it has the most context on the change. The configured order only
|
|
307
|
+
// decides who runs when the author is human or that agent is unavailable.
|
|
308
|
+
if (originVendor && config.vendors[originVendor].enabled && supportsStep(originVendor, stepType))
|
|
309
|
+
return originVendor;
|
|
310
|
+
if (writesCode) {
|
|
311
|
+
const preferred = priorityOrder(task, config, stepType)[0];
|
|
312
|
+
if (preferred)
|
|
313
|
+
return preferred;
|
|
314
|
+
}
|
|
261
315
|
return fallback && config.vendors[fallback].enabled ? fallback : null;
|
|
262
316
|
}
|
|
263
317
|
if (reviewer === 'auto') {
|
|
264
|
-
if (
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (config.vendors.claude.enabled)
|
|
279
|
-
return 'claude';
|
|
280
|
-
if (config.vendors.opencode.enabled)
|
|
281
|
-
return 'opencode';
|
|
282
|
-
return null;
|
|
318
|
+
if (writesCode && originVendor && config.vendors[originVendor].enabled && supportsStep(originVendor, stepType)) {
|
|
319
|
+
return originVendor;
|
|
320
|
+
}
|
|
321
|
+
let ordered = priorityOrder(task, config, stepType);
|
|
322
|
+
if (ordered.length === 0)
|
|
323
|
+
return legacyAutoReviewer(origin, config);
|
|
324
|
+
// Review is cross-vendor even when the author's agent tops the list. Fall
|
|
325
|
+
// back to it only when it is the sole enabled/capable vendor.
|
|
326
|
+
if (!writesCode && ordered.length > 1 && originVendor) {
|
|
327
|
+
const crossVendor = ordered.filter(vendor => vendor !== originVendor);
|
|
328
|
+
if (crossVendor.length > 0)
|
|
329
|
+
ordered = crossVendor;
|
|
330
|
+
}
|
|
331
|
+
return ordered[0] ?? null;
|
|
283
332
|
}
|
|
284
333
|
if (reviewer === 'claude')
|
|
285
334
|
return config.vendors.claude.enabled ? 'claude' : (fallback && config.vendors[fallback].enabled ? fallback : null);
|
|
@@ -289,13 +338,19 @@ function resolveReviewer(reviewer, origin, config, fallback) {
|
|
|
289
338
|
return config.vendors.opencode.enabled ? 'opencode' : (fallback && config.vendors[fallback].enabled ? fallback : null);
|
|
290
339
|
return null;
|
|
291
340
|
}
|
|
341
|
+
// Exported for tests and for review call sites that need the cross-vendor rule.
|
|
342
|
+
export function resolveReviewVendor(stepReviewer, origin, config, fallback) {
|
|
343
|
+
return resolveReviewer(stepReviewer, origin, config, fallback, 'review');
|
|
344
|
+
}
|
|
292
345
|
function supportsStep(vendor, stepType) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// Conflict resolution is Claude-only until Codex/OpenCode conflict resolution exists.
|
|
296
|
-
return vendor === 'claude';
|
|
346
|
+
void vendor;
|
|
347
|
+
return stepType === 'review' || stepType === 'recheck' || stepType === 'fix' || stepType === 'conflict-resolve';
|
|
297
348
|
}
|
|
298
349
|
function resolveLimitFallbackVendor(failedVendor, stepType, config) {
|
|
350
|
+
const ordered = priorityOrder(priorityTaskForStep(stepType), config, stepType)
|
|
351
|
+
.filter(vendor => vendor !== failedVendor);
|
|
352
|
+
if (ordered.length > 0)
|
|
353
|
+
return ordered[0];
|
|
299
354
|
const candidates = failedVendor === 'claude'
|
|
300
355
|
? ['codex', 'opencode']
|
|
301
356
|
: failedVendor === 'codex'
|
|
@@ -340,16 +395,12 @@ export function conflictResolveCommitSubject(conflictCount, vendor) {
|
|
|
340
395
|
// unsupported-step skip instead of silently substituting a different vendor.
|
|
341
396
|
// null disables the fallback entirely.
|
|
342
397
|
function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
|
|
343
|
-
const vendor = resolveReviewer(stepReviewer, origin, config, fallback);
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
// and the auto fallback picked Claude.) Substitute a capable, enabled vendor.
|
|
350
|
-
// Scoped to origin-derived assignment only — an explicit reviewer: claude|codex,
|
|
351
|
-
// reviewer: auto, or routing.fallback_reviewer is an operator decision, left as
|
|
352
|
-
// written so the caller can report the precise unsupported-step skip.
|
|
398
|
+
const vendor = resolveReviewer(stepReviewer, origin, config, fallback, stepType);
|
|
399
|
+
// Every current step supports every vendor, so this substitution is a guard
|
|
400
|
+
// for future step capabilities rather than a live path. It stays scoped to
|
|
401
|
+
// origin-derived assignment: an explicit reviewer choice or
|
|
402
|
+
// routing.fallback_reviewer is an operator decision and must be reported
|
|
403
|
+
// precisely instead of silently swapped.
|
|
353
404
|
if (stepReviewer === 'origin' &&
|
|
354
405
|
(origin === 'claude' || origin === 'codex' || origin === 'opencode') &&
|
|
355
406
|
vendor !== null &&
|
|
@@ -359,6 +410,13 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
|
|
|
359
410
|
return { vendor: capable, usedHumanFallback: false, substitutedOriginVendor: vendor };
|
|
360
411
|
}
|
|
361
412
|
}
|
|
413
|
+
// A configured task order is the human-origin fallback now, so report it as
|
|
414
|
+
// one: the fix/conflict-resolve callers use this flag for their fallback log.
|
|
415
|
+
const humanOriginPriority = stepReviewer === 'origin' && origin === 'human'
|
|
416
|
+
&& priorityOrder(priorityTaskForStep(stepType), config, stepType).length > 0;
|
|
417
|
+
if (vendor !== null && humanOriginPriority) {
|
|
418
|
+
return { vendor, usedHumanFallback: true };
|
|
419
|
+
}
|
|
362
420
|
if (vendor !== null || origin !== 'human' || stepReviewer !== 'origin') {
|
|
363
421
|
return { vendor, usedHumanFallback: false };
|
|
364
422
|
}
|
|
@@ -371,10 +429,12 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
|
|
|
371
429
|
else if (fb === 'opencode')
|
|
372
430
|
humanFallback = config.vendors.opencode.enabled ? 'opencode' : null;
|
|
373
431
|
else if (fb !== null) {
|
|
374
|
-
// 'auto':
|
|
375
|
-
//
|
|
432
|
+
// 'auto': the configured task order wins; with no order configured, keep
|
|
433
|
+
// the legacy codex-first preference. Either way, narrow to vendors that
|
|
434
|
+
// support this step type.
|
|
376
435
|
const usable = (v) => config.vendors[v].enabled && supportsStep(v, stepType);
|
|
377
|
-
humanFallback =
|
|
436
|
+
humanFallback = priorityOrder(priorityTaskForStep(stepType), config, stepType)[0]
|
|
437
|
+
?? (usable('codex') ? 'codex' : usable('claude') ? 'claude' : usable('opencode') ? 'opencode' : null);
|
|
378
438
|
}
|
|
379
439
|
if (!humanFallback)
|
|
380
440
|
return { vendor: null, usedHumanFallback: false };
|
|
@@ -950,7 +1010,7 @@ export async function runWorkflow(ctx) {
|
|
|
950
1010
|
}
|
|
951
1011
|
if (effectiveType === 'review' || effectiveType === 'recheck') {
|
|
952
1012
|
const isRecheck = effectiveType === 'recheck';
|
|
953
|
-
let reviewer =
|
|
1013
|
+
let reviewer = resolveReviewVendor(step.reviewer, origin, config, ctx.smartSwitchFallback);
|
|
954
1014
|
if (!reviewer) {
|
|
955
1015
|
fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'no_reviewer' });
|
|
956
1016
|
results[step.name] = { skipped: true, skipReason: 'no_reviewer' };
|
|
@@ -1963,27 +2023,14 @@ export async function runWorkflow(ctx) {
|
|
|
1963
2023
|
skipConflictResolve('no_vendor');
|
|
1964
2024
|
continue;
|
|
1965
2025
|
}
|
|
1966
|
-
if (vendor === 'codex') {
|
|
1967
|
-
try {
|
|
1968
|
-
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1969
|
-
}
|
|
1970
|
-
catch { /* ignore */ }
|
|
1971
|
-
;
|
|
1972
|
-
skipConflictResolve('codex_conflict_resolve_unsupported');
|
|
1973
|
-
continue;
|
|
1974
|
-
}
|
|
1975
|
-
if (vendor === 'opencode') {
|
|
1976
|
-
try {
|
|
1977
|
-
runGitWithoutHooks(tmpDir, ['merge', '--abort']);
|
|
1978
|
-
}
|
|
1979
|
-
catch { /* ignore */ }
|
|
1980
|
-
;
|
|
1981
|
-
skipConflictResolve('opencode_conflict_resolve_unsupported');
|
|
1982
|
-
continue;
|
|
1983
|
-
}
|
|
1984
2026
|
// Conflict-resolve is mechanical text surgery bounded by the markers —
|
|
1985
2027
|
// measured at 37s against ~643s for a review — so it always runs fast.
|
|
1986
|
-
const
|
|
2028
|
+
const conflictResolveQuality = { ...config.quality, tier: config.quality.mode === 'smart' ? 'fast' : config.quality.tier };
|
|
2029
|
+
const conflictResolveModel = vendor === 'claude'
|
|
2030
|
+
? resolveClaudeModel(conflictResolveQuality, config.vendors.claude)
|
|
2031
|
+
: vendor === 'codex'
|
|
2032
|
+
? resolveCodexModel(conflictResolveQuality, config.vendors.codex)
|
|
2033
|
+
: resolveOpenCodeModel(conflictResolveQuality, config.vendors.opencode);
|
|
1987
2034
|
const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
|
|
1988
2035
|
if (isFork) {
|
|
1989
2036
|
try {
|
|
@@ -2016,7 +2063,7 @@ export async function runWorkflow(ctx) {
|
|
|
2016
2063
|
const skillSession = skillSessionFor(step.name, effectiveType);
|
|
2017
2064
|
try {
|
|
2018
2065
|
;
|
|
2019
|
-
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed, effort: resolveEffort } = await
|
|
2066
|
+
({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed, effort: resolveEffort } = await runConflictResolveForVendor(vendor, tmpDir, pr.title, step.instructions ?? '', conflictResolveModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors[vendor].timeout_sec), skillSession, config.vendors[vendor].effort, config.skills.codex_full_access));
|
|
2020
2067
|
}
|
|
2021
2068
|
catch (err) {
|
|
2022
2069
|
logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
|
|
@@ -2106,7 +2153,7 @@ export async function runWorkflow(ctx) {
|
|
|
2106
2153
|
'-m',
|
|
2107
2154
|
conflictResolveCommitSubject(conflictedFiles.length, vendor),
|
|
2108
2155
|
'-m',
|
|
2109
|
-
buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel, step: 'conflict-resolve', service: 'crosscheck' }),
|
|
2156
|
+
buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel ?? 'default', step: 'conflict-resolve', service: 'crosscheck' }),
|
|
2110
2157
|
]);
|
|
2111
2158
|
const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
|
|
2112
2159
|
await pushWithNonFastForwardHandling({
|
|
@@ -2149,6 +2196,7 @@ export async function runWorkflow(ctx) {
|
|
|
2149
2196
|
effort: resolveEffort,
|
|
2150
2197
|
author: authorAttribution ?? undefined,
|
|
2151
2198
|
skills: activatedSkills,
|
|
2199
|
+
vendor,
|
|
2152
2200
|
});
|
|
2153
2201
|
await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
|
|
2154
2202
|
fileLog({ level: 'info', event: 'conflict_resolved_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });
|