@humanbased/crosscheck 1.7.2-beta.9 → 1.8.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/crosscheck.config.example.yml +23 -0
  2. package/dist/__tests__/author-attribution.test.d.ts +2 -0
  3. package/dist/__tests__/author-attribution.test.d.ts.map +1 -0
  4. package/dist/__tests__/author-attribution.test.js +89 -0
  5. package/dist/__tests__/author-attribution.test.js.map +1 -0
  6. package/dist/__tests__/comment-bodies.test.js +42 -0
  7. package/dist/__tests__/comment-bodies.test.js.map +1 -1
  8. package/dist/__tests__/conflict-resolve.test.js +91 -1
  9. package/dist/__tests__/conflict-resolve.test.js.map +1 -1
  10. package/dist/__tests__/onboard-preservation.test.js +15 -0
  11. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  12. package/dist/__tests__/opencode-policy.test.d.ts +2 -0
  13. package/dist/__tests__/opencode-policy.test.d.ts.map +1 -0
  14. package/dist/__tests__/opencode-policy.test.js +60 -0
  15. package/dist/__tests__/opencode-policy.test.js.map +1 -0
  16. package/dist/__tests__/optimize.test.js +1 -0
  17. package/dist/__tests__/optimize.test.js.map +1 -1
  18. package/dist/__tests__/review-comment-body.test.js +22 -0
  19. package/dist/__tests__/review-comment-body.test.js.map +1 -1
  20. package/dist/__tests__/runner.test.js +65 -21
  21. package/dist/__tests__/runner.test.js.map +1 -1
  22. package/dist/commands/issue.d.ts.map +1 -1
  23. package/dist/commands/issue.js +5 -3
  24. package/dist/commands/issue.js.map +1 -1
  25. package/dist/commands/onboard.d.ts +3 -0
  26. package/dist/commands/onboard.d.ts.map +1 -1
  27. package/dist/commands/onboard.js +92 -12
  28. package/dist/commands/onboard.js.map +1 -1
  29. package/dist/commands/optimize.d.ts.map +1 -1
  30. package/dist/commands/optimize.js +5 -3
  31. package/dist/commands/optimize.js.map +1 -1
  32. package/dist/commands/review.d.ts.map +1 -1
  33. package/dist/commands/review.js +2 -1
  34. package/dist/commands/review.js.map +1 -1
  35. package/dist/config/schema.d.ts +37 -0
  36. package/dist/config/schema.d.ts.map +1 -1
  37. package/dist/config/schema.js +14 -0
  38. package/dist/config/schema.js.map +1 -1
  39. package/dist/github/client.d.ts +4 -1
  40. package/dist/github/client.d.ts.map +1 -1
  41. package/dist/github/client.js +5 -3
  42. package/dist/github/client.js.map +1 -1
  43. package/dist/lib/author-attribution.d.ts +10 -0
  44. package/dist/lib/author-attribution.d.ts.map +1 -0
  45. package/dist/lib/author-attribution.js +94 -0
  46. package/dist/lib/author-attribution.js.map +1 -0
  47. package/dist/lib/comment-bodies.d.ts +10 -0
  48. package/dist/lib/comment-bodies.d.ts.map +1 -1
  49. package/dist/lib/comment-bodies.js +45 -9
  50. package/dist/lib/comment-bodies.js.map +1 -1
  51. package/dist/lib/runner.d.ts +1 -0
  52. package/dist/lib/runner.d.ts.map +1 -1
  53. package/dist/lib/runner.js +123 -67
  54. package/dist/lib/runner.js.map +1 -1
  55. package/dist/reviewers/conflict-resolve.d.ts +7 -3
  56. package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
  57. package/dist/reviewers/conflict-resolve.js +199 -11
  58. package/dist/reviewers/conflict-resolve.js.map +1 -1
  59. package/dist/reviewers/fix.d.ts.map +1 -1
  60. package/dist/reviewers/fix.js +4 -2
  61. package/dist/reviewers/fix.js.map +1 -1
  62. package/dist/reviewers/opencode.d.ts +3 -2
  63. package/dist/reviewers/opencode.d.ts.map +1 -1
  64. package/dist/reviewers/opencode.js +31 -16
  65. package/dist/reviewers/opencode.js.map +1 -1
  66. package/get-started.md +21 -1
  67. package/package.json +1 -1
@@ -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 { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
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';
@@ -37,6 +37,7 @@ import { tierTimeoutMs } from '../reviewers/tier-timeouts.js';
37
37
  import { loadSkillCatalog } from '../skills/catalog.js';
38
38
  import { createSkillActivationSession } from '../skills/broker.js';
39
39
  import { formatSkillAttribution } from '../skills/attribution.js';
40
+ import { parseAuthorAttribution } from './author-attribution.js';
40
41
  import { CompromisedCloneError, runGitWithoutHooks, changedFilesVsBase } from './clone.js';
41
42
  const MAX_CROSSCHECK_COMMITS = 5;
42
43
  const FIX_RETRY_DELAY_MS = 2 * 60 * 1000;
@@ -249,36 +250,85 @@ function countComments(reviewText) {
249
250
  const numbered = (reviewText.match(/^\d+\.\s/gm) ?? []).length;
250
251
  return bullets + numbered;
251
252
  }
252
- function resolveReviewer(reviewer, origin, config, fallback) {
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';
253
304
  if (reviewer === 'origin') {
254
- if (origin === 'claude' && config.vendors.claude.enabled)
255
- return 'claude';
256
- if (origin === 'codex' && config.vendors.codex.enabled)
257
- return 'codex';
258
- if (origin === 'opencode' && config.vendors.opencode.enabled)
259
- return 'opencode';
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
+ }
260
315
  return fallback && config.vendors[fallback].enabled ? fallback : null;
261
316
  }
262
317
  if (reviewer === 'auto') {
263
- if (origin === 'claude' && config.vendors.codex.enabled)
264
- return 'codex';
265
- if (origin === 'claude' && config.vendors.opencode.enabled)
266
- return 'opencode';
267
- if (origin === 'codex' && config.vendors.claude.enabled)
268
- return 'claude';
269
- if (origin === 'codex' && config.vendors.opencode.enabled)
270
- return 'opencode';
271
- if (origin === 'opencode' && config.vendors.claude.enabled)
272
- return 'claude';
273
- if (origin === 'opencode' && config.vendors.codex.enabled)
274
- return 'codex';
275
- if (config.vendors.codex.enabled)
276
- return 'codex';
277
- if (config.vendors.claude.enabled)
278
- return 'claude';
279
- if (config.vendors.opencode.enabled)
280
- return 'opencode';
281
- 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;
282
332
  }
283
333
  if (reviewer === 'claude')
284
334
  return config.vendors.claude.enabled ? 'claude' : (fallback && config.vendors[fallback].enabled ? fallback : null);
@@ -288,13 +338,19 @@ function resolveReviewer(reviewer, origin, config, fallback) {
288
338
  return config.vendors.opencode.enabled ? 'opencode' : (fallback && config.vendors[fallback].enabled ? fallback : null);
289
339
  return null;
290
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
+ }
291
345
  function supportsStep(vendor, stepType) {
292
- if (stepType === 'review' || stepType === 'recheck' || stepType === 'fix')
293
- return true;
294
- // Conflict resolution is Claude-only until Codex/OpenCode conflict resolution exists.
295
- return vendor === 'claude';
346
+ void vendor;
347
+ return stepType === 'review' || stepType === 'recheck' || stepType === 'fix' || stepType === 'conflict-resolve';
296
348
  }
297
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];
298
354
  const candidates = failedVendor === 'claude'
299
355
  ? ['codex', 'opencode']
300
356
  : failedVendor === 'codex'
@@ -339,16 +395,12 @@ export function conflictResolveCommitSubject(conflictCount, vendor) {
339
395
  // unsupported-step skip instead of silently substituting a different vendor.
340
396
  // null disables the fallback entirely.
341
397
  function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
342
- const vendor = resolveReviewer(stepReviewer, origin, config, fallback);
343
- // Origin detection can assign a vendor that cannot run the step: conflict
344
- // resolution is Claude-only, so a Codex-origin PR (reviewer: 'origin', origin:
345
- // 'codex') resolves to 'codex' and the dispatch skips it as unsupported — the
346
- // conflicts never get resolved. (#284's `Crosscheck-Reviewer: codex` detection
347
- // introduced this: these crosscheck-authored fix PRs used to detect as 'human'
348
- // and the auto fallback picked Claude.) Substitute a capable, enabled vendor.
349
- // Scoped to origin-derived assignment only — an explicit reviewer: claude|codex,
350
- // reviewer: auto, or routing.fallback_reviewer is an operator decision, left as
351
- // 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.
352
404
  if (stepReviewer === 'origin' &&
353
405
  (origin === 'claude' || origin === 'codex' || origin === 'opencode') &&
354
406
  vendor !== null &&
@@ -358,6 +410,13 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
358
410
  return { vendor: capable, usedHumanFallback: false, substitutedOriginVendor: vendor };
359
411
  }
360
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
+ }
361
420
  if (vendor !== null || origin !== 'human' || stepReviewer !== 'origin') {
362
421
  return { vendor, usedHumanFallback: false };
363
422
  }
@@ -370,10 +429,12 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
370
429
  else if (fb === 'opencode')
371
430
  humanFallback = config.vendors.opencode.enabled ? 'opencode' : null;
372
431
  else if (fb !== null) {
373
- // 'auto': prefer codex then claude then opencode, same as resolveReviewer's
374
- // auto path, narrowed to vendors that support this step type.
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.
375
435
  const usable = (v) => config.vendors[v].enabled && supportsStep(v, stepType);
376
- humanFallback = usable('codex') ? 'codex' : usable('claude') ? 'claude' : usable('opencode') ? 'opencode' : null;
436
+ humanFallback = priorityOrder(priorityTaskForStep(stepType), config, stepType)[0]
437
+ ?? (usable('codex') ? 'codex' : usable('claude') ? 'claude' : usable('opencode') ? 'opencode' : null);
377
438
  }
378
439
  if (!humanFallback)
379
440
  return { vendor: null, usedHumanFallback: false };
@@ -741,6 +802,9 @@ export async function pushWithNonFastForwardHandling(params) {
741
802
  export async function runWorkflow(ctx) {
742
803
  const { owner, repoName, prNumber, pr, tmpDir, token, config, origin, log, onPhaseChange, trigger } = ctx;
743
804
  const triggerField = trigger !== undefined ? { trigger } : {};
805
+ // The author's declaration lives in the PR body. Parsing it once here means
806
+ // every card this run posts names the same author, model and effort.
807
+ const authorAttribution = parseAuthorAttribution(pr.body ?? '');
744
808
  const configuredSteps = (ctx.steps ?? loadWorkflow(process.cwd())).map(step => {
745
809
  if (!step.harness || step.instructions)
746
810
  return step;
@@ -946,7 +1010,7 @@ export async function runWorkflow(ctx) {
946
1010
  }
947
1011
  if (effectiveType === 'review' || effectiveType === 'recheck') {
948
1012
  const isRecheck = effectiveType === 'recheck';
949
- let reviewer = resolveReviewer(step.reviewer, origin, config, ctx.smartSwitchFallback);
1013
+ let reviewer = resolveReviewVendor(step.reviewer, origin, config, ctx.smartSwitchFallback);
950
1014
  if (!reviewer) {
951
1015
  fileLog({ level: 'info', event: 'step_skipped', repo: `${owner}/${repoName}`, pr: prNumber, step: step.name, reason: 'no_reviewer' });
952
1016
  results[step.name] = { skipped: true, skipReason: 'no_reviewer' };
@@ -1201,7 +1265,7 @@ export async function runWorkflow(ctx) {
1201
1265
  // Withheld when an explicit vendors.*.model overrode the tier map:
1202
1266
  // citing a tier the run did not use would assert a routing decision
1203
1267
  // that never happened.
1204
- strategyCitation(reviewer === 'codex' ? config.vendors.codex : reviewer === 'opencode' ? config.vendors.opencode : config.vendors.claude, strategy, roundStrategy, model));
1268
+ strategyCitation(reviewer === 'codex' ? config.vendors.codex : reviewer === 'opencode' ? config.vendors.opencode : config.vendors.claude, strategy, roundStrategy, model), authorAttribution ?? undefined);
1205
1269
  if (memoryPlan && structured?.snapshot) {
1206
1270
  // The review is already posted; a memory write failure only costs the next review its delta.
1207
1271
  try {
@@ -1473,6 +1537,7 @@ export async function runWorkflow(ctx) {
1473
1537
  vendor: activeVendor,
1474
1538
  model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1475
1539
  effort: fixEffort,
1540
+ author: authorAttribution ?? undefined,
1476
1541
  skills: activatedSkills,
1477
1542
  }),
1478
1543
  });
@@ -1559,6 +1624,7 @@ export async function runWorkflow(ctx) {
1559
1624
  reviewCommentBody,
1560
1625
  model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1561
1626
  effort: fixEffort,
1627
+ author: authorAttribution ?? undefined,
1562
1628
  skills: activatedSkills,
1563
1629
  });
1564
1630
  await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
@@ -1632,6 +1698,7 @@ export async function runWorkflow(ctx) {
1632
1698
  reviewCommentBody,
1633
1699
  model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1634
1700
  effort: fixEffort,
1701
+ author: authorAttribution ?? undefined,
1635
1702
  skills: activatedSkills,
1636
1703
  });
1637
1704
  await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
@@ -1956,27 +2023,14 @@ export async function runWorkflow(ctx) {
1956
2023
  skipConflictResolve('no_vendor');
1957
2024
  continue;
1958
2025
  }
1959
- if (vendor === 'codex') {
1960
- try {
1961
- runGitWithoutHooks(tmpDir, ['merge', '--abort']);
1962
- }
1963
- catch { /* ignore */ }
1964
- ;
1965
- skipConflictResolve('codex_conflict_resolve_unsupported');
1966
- continue;
1967
- }
1968
- if (vendor === 'opencode') {
1969
- try {
1970
- runGitWithoutHooks(tmpDir, ['merge', '--abort']);
1971
- }
1972
- catch { /* ignore */ }
1973
- ;
1974
- skipConflictResolve('opencode_conflict_resolve_unsupported');
1975
- continue;
1976
- }
1977
2026
  // Conflict-resolve is mechanical text surgery bounded by the markers —
1978
2027
  // measured at 37s against ~643s for a review — so it always runs fast.
1979
- const conflictResolveModel = resolveClaudeModel({ ...config.quality, tier: config.quality.mode === 'smart' ? 'fast' : config.quality.tier }, config.vendors.claude);
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);
1980
2034
  const isFork = pr.head.repo?.full_name !== pr.base.repo.full_name;
1981
2035
  if (isFork) {
1982
2036
  try {
@@ -2009,7 +2063,7 @@ export async function runWorkflow(ctx) {
2009
2063
  const skillSession = skillSessionFor(step.name, effectiveType);
2010
2064
  try {
2011
2065
  ;
2012
- ({ appliedCount, resolvedPaths, tokensUsed: resolveTokensUsed, effort: resolveEffort } = await runConflictResolveStep(tmpDir, pr.title, step.instructions ?? '', conflictResolveModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), skillSession, config.vendors.claude.effort));
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));
2013
2067
  }
2014
2068
  catch (err) {
2015
2069
  logError({ repo: `${owner}/${repoName}`, pr: prNumber, phase: 'conflict-resolve', attempt: 1 }, err);
@@ -2099,7 +2153,7 @@ export async function runWorkflow(ctx) {
2099
2153
  '-m',
2100
2154
  conflictResolveCommitSubject(conflictedFiles.length, vendor),
2101
2155
  '-m',
2102
- buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel, step: 'conflict-resolve', service: 'crosscheck' }),
2156
+ buildCommitTrailers({ reviewer: vendor, model: conflictResolveModel ?? 'default', step: 'conflict-resolve', service: 'crosscheck' }),
2103
2157
  ]);
2104
2158
  const newSha = execSync('git rev-parse HEAD', { cwd: tmpDir, encoding: 'utf8' }).trim();
2105
2159
  await pushWithNonFastForwardHandling({
@@ -2140,7 +2194,9 @@ export async function runWorkflow(ctx) {
2140
2194
  files: resolvedPaths.length > 0 ? resolvedPaths : conflictedFiles,
2141
2195
  model: conflictResolveModel,
2142
2196
  effort: resolveEffort,
2197
+ author: authorAttribution ?? undefined,
2143
2198
  skills: activatedSkills,
2199
+ vendor,
2144
2200
  });
2145
2201
  await octokit.rest.issues.createComment({ owner, repo: repoName, issue_number: prNumber, body });
2146
2202
  fileLog({ level: 'info', event: 'conflict_resolved_comment_posted', repo: `${owner}/${repoName}`, pr: prNumber, sha: newSha });