@humanbased/crosscheck 1.6.1-beta.5 → 1.7.0-beta.22

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 (92) hide show
  1. package/README.md +3 -1
  2. package/crosscheck.config.example.yml +20 -4
  3. package/dist/__tests__/detector.test.js +22 -0
  4. package/dist/__tests__/detector.test.js.map +1 -1
  5. package/dist/__tests__/onboard-preservation.test.js +27 -5
  6. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  7. package/dist/__tests__/optimize.test.js +18 -2
  8. package/dist/__tests__/optimize.test.js.map +1 -1
  9. package/dist/__tests__/review-models.test.js +5 -5
  10. package/dist/__tests__/review-models.test.js.map +1 -1
  11. package/dist/__tests__/review-strategy.test.js +10 -10
  12. package/dist/__tests__/review-strategy.test.js.map +1 -1
  13. package/dist/__tests__/runner.test.js +12 -2
  14. package/dist/__tests__/runner.test.js.map +1 -1
  15. package/dist/commands/init.d.ts.map +1 -1
  16. package/dist/commands/init.js +14 -1
  17. package/dist/commands/init.js.map +1 -1
  18. package/dist/commands/issue.d.ts.map +1 -1
  19. package/dist/commands/issue.js +40 -16
  20. package/dist/commands/issue.js.map +1 -1
  21. package/dist/commands/onboard.d.ts +5 -4
  22. package/dist/commands/onboard.d.ts.map +1 -1
  23. package/dist/commands/onboard.js +126 -48
  24. package/dist/commands/onboard.js.map +1 -1
  25. package/dist/commands/optimize.d.ts +1 -1
  26. package/dist/commands/optimize.d.ts.map +1 -1
  27. package/dist/commands/optimize.js +53 -13
  28. package/dist/commands/optimize.js.map +1 -1
  29. package/dist/commands/review.d.ts.map +1 -1
  30. package/dist/commands/review.js +9 -3
  31. package/dist/commands/review.js.map +1 -1
  32. package/dist/commands/run.d.ts +2 -2
  33. package/dist/commands/run.d.ts.map +1 -1
  34. package/dist/commands/run.js +1 -1
  35. package/dist/commands/run.js.map +1 -1
  36. package/dist/commands/status.d.ts.map +1 -1
  37. package/dist/commands/status.js +12 -1
  38. package/dist/commands/status.js.map +1 -1
  39. package/dist/commands/watch.d.ts.map +1 -1
  40. package/dist/commands/watch.js +6 -3
  41. package/dist/commands/watch.js.map +1 -1
  42. package/dist/config/review-model-tiers.json +9 -9
  43. package/dist/config/review-strategy.json +12 -12
  44. package/dist/config/schema.d.ts +102 -16
  45. package/dist/config/schema.d.ts.map +1 -1
  46. package/dist/config/schema.js +28 -3
  47. package/dist/config/schema.js.map +1 -1
  48. package/dist/github/client.js +1 -1
  49. package/dist/github/client.js.map +1 -1
  50. package/dist/github/detector.d.ts +2 -2
  51. package/dist/github/detector.d.ts.map +1 -1
  52. package/dist/github/detector.js +46 -11
  53. package/dist/github/detector.js.map +1 -1
  54. package/dist/lib/comment-bodies.d.ts.map +1 -1
  55. package/dist/lib/comment-bodies.js +4 -2
  56. package/dist/lib/comment-bodies.js.map +1 -1
  57. package/dist/lib/review-models.d.ts +4 -1
  58. package/dist/lib/review-models.d.ts.map +1 -1
  59. package/dist/lib/review-models.js +8 -0
  60. package/dist/lib/review-models.js.map +1 -1
  61. package/dist/lib/reviewer-error.d.ts.map +1 -1
  62. package/dist/lib/reviewer-error.js +12 -8
  63. package/dist/lib/reviewer-error.js.map +1 -1
  64. package/dist/lib/runner.d.ts +8 -7
  65. package/dist/lib/runner.d.ts.map +1 -1
  66. package/dist/lib/runner.js +71 -20
  67. package/dist/lib/runner.js.map +1 -1
  68. package/dist/lib/smart-switch.d.ts +7 -6
  69. package/dist/lib/smart-switch.d.ts.map +1 -1
  70. package/dist/lib/smart-switch.js +9 -2
  71. package/dist/lib/smart-switch.js.map +1 -1
  72. package/dist/lib/vendor.d.ts +2 -2
  73. package/dist/lib/vendor.d.ts.map +1 -1
  74. package/dist/lib/vendor.js +12 -2
  75. package/dist/lib/vendor.js.map +1 -1
  76. package/dist/lib/workflow.d.ts +8 -8
  77. package/dist/lib/workflow.js +1 -1
  78. package/dist/lib/workflow.js.map +1 -1
  79. package/dist/reviewers/fix.d.ts +6 -0
  80. package/dist/reviewers/fix.d.ts.map +1 -1
  81. package/dist/reviewers/fix.js +61 -0
  82. package/dist/reviewers/fix.js.map +1 -1
  83. package/dist/reviewers/opencode-env.d.ts +13 -0
  84. package/dist/reviewers/opencode-env.d.ts.map +1 -0
  85. package/dist/reviewers/opencode-env.js +52 -0
  86. package/dist/reviewers/opencode-env.js.map +1 -0
  87. package/dist/reviewers/opencode.d.ts +17 -0
  88. package/dist/reviewers/opencode.d.ts.map +1 -0
  89. package/dist/reviewers/opencode.js +293 -0
  90. package/dist/reviewers/opencode.js.map +1 -0
  91. package/get-started.md +38 -4
  92. package/package.json +1 -1
@@ -7,7 +7,8 @@ import { filterStepsByTypes } from './repo-workflow.js';
7
7
  import { vendorDisplayName } from '../lib/vendor.js';
8
8
  import { runCodexReview } from '../reviewers/codex.js';
9
9
  import { runClaudeReview } from '../reviewers/claude.js';
10
- import { runFixStep, runCodexFixStep } from '../reviewers/fix.js';
10
+ import { runOpenCodeReview } from '../reviewers/opencode.js';
11
+ import { runFixStep, runCodexFixStep, runOpenCodeFixStep } from '../reviewers/fix.js';
11
12
  import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
12
13
  import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE, DOC_ONLY_GATE_NOTE, detectInconclusiveReview, reviewHasNoFindings } from '../lib/verdict.js';
13
14
  import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
@@ -22,9 +23,9 @@ import { acquireRemoteLock, releaseRemoteLock } from '../github/review-status.js
22
23
  import { log as fileLog, logError, classifyError } from '../lib/logger.js';
23
24
  import { buildCommitTrailers, parseAnnotation } from '../lib/annotation.js';
24
25
  import { shaCovers } from '../lib/pr-workflow-state.js';
25
- import { resolveClaudeModel, resolveCodexModel } from '../lib/review-models.js';
26
+ import { resolveClaudeModel, resolveCodexModel, resolveOpenCodeModel } from '../lib/review-models.js';
26
27
  import { resolveReviewStrategy, escalate, clampToLevels, isDocOnlyChange } from './review-strategy.js';
27
- import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS } from '../config/schema.js';
28
+ import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS, OPENCODE_EFFORT_LEVELS } from '../config/schema.js';
28
29
  import { buildStepIdentityFields } from '../lib/event-fields.js';
29
30
  import { planAutoFixDelivery, forceWithLeaseArgs, parseLsRemoteOid, isLeaseRejection, assessFixBranchOwnership, isInvalidBaseError } from '../lib/auto-fix-branch.js';
30
31
  import { prOpenToVerdictMs } from '../lib/adoption.js';
@@ -254,34 +255,56 @@ function resolveReviewer(reviewer, origin, config, fallback) {
254
255
  return 'claude';
255
256
  if (origin === 'codex' && config.vendors.codex.enabled)
256
257
  return 'codex';
258
+ if (origin === 'opencode' && config.vendors.opencode.enabled)
259
+ return 'opencode';
257
260
  return fallback && config.vendors[fallback].enabled ? fallback : null;
258
261
  }
259
262
  if (reviewer === 'auto') {
260
263
  if (origin === 'claude' && config.vendors.codex.enabled)
261
264
  return 'codex';
265
+ if (origin === 'claude' && config.vendors.opencode.enabled)
266
+ return 'opencode';
262
267
  if (origin === 'codex' && config.vendors.claude.enabled)
263
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';
264
275
  if (config.vendors.codex.enabled)
265
276
  return 'codex';
266
277
  if (config.vendors.claude.enabled)
267
278
  return 'claude';
279
+ if (config.vendors.opencode.enabled)
280
+ return 'opencode';
268
281
  return null;
269
282
  }
270
283
  if (reviewer === 'claude')
271
284
  return config.vendors.claude.enabled ? 'claude' : (fallback && config.vendors[fallback].enabled ? fallback : null);
272
285
  if (reviewer === 'codex')
273
286
  return config.vendors.codex.enabled ? 'codex' : (fallback && config.vendors[fallback].enabled ? fallback : null);
287
+ if (reviewer === 'opencode')
288
+ return config.vendors.opencode.enabled ? 'opencode' : (fallback && config.vendors[fallback].enabled ? fallback : null);
274
289
  return null;
275
290
  }
276
291
  function supportsStep(vendor, stepType) {
277
292
  if (stepType === 'review' || stepType === 'recheck' || stepType === 'fix')
278
293
  return true;
279
- // Conflict resolution is Claude-only until Codex conflict resolution exists.
294
+ // Conflict resolution is Claude-only until Codex/OpenCode conflict resolution exists.
280
295
  return vendor === 'claude';
281
296
  }
282
297
  function resolveLimitFallbackVendor(failedVendor, stepType, config) {
283
- const fallback = failedVendor === 'claude' ? 'codex' : 'claude';
284
- return config.vendors[fallback].enabled && supportsStep(fallback, stepType) ? fallback : null;
298
+ const candidates = failedVendor === 'claude'
299
+ ? ['codex', 'opencode']
300
+ : failedVendor === 'codex'
301
+ ? ['claude', 'opencode']
302
+ : ['claude', 'codex'];
303
+ for (const candidate of candidates) {
304
+ if (config.vendors[candidate].enabled && supportsStep(candidate, stepType))
305
+ return candidate;
306
+ }
307
+ return null;
285
308
  }
286
309
  function notifyVendorFailure(ctx, failedVendor, fallbackVendor, reason, stepName) {
287
310
  const handler = ctx.onVendorFailure ?? ctx.onVendorLimit;
@@ -327,7 +350,7 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
327
350
  // reviewer: auto, or routing.fallback_reviewer is an operator decision, left as
328
351
  // written so the caller can report the precise unsupported-step skip.
329
352
  if (stepReviewer === 'origin' &&
330
- (origin === 'claude' || origin === 'codex') &&
353
+ (origin === 'claude' || origin === 'codex' || origin === 'opencode') &&
331
354
  vendor !== null &&
332
355
  !supportsStep(vendor, stepType)) {
333
356
  const capable = vendor === 'claude' ? 'codex' : 'claude';
@@ -344,11 +367,13 @@ function resolveStepVendor(stepType, stepReviewer, origin, config, fallback) {
344
367
  humanFallback = config.vendors.claude.enabled ? 'claude' : null;
345
368
  else if (fb === 'codex')
346
369
  humanFallback = config.vendors.codex.enabled ? 'codex' : null;
370
+ else if (fb === 'opencode')
371
+ humanFallback = config.vendors.opencode.enabled ? 'opencode' : null;
347
372
  else if (fb !== null) {
348
- // 'auto': prefer codex then claude, same as resolveReviewer's auto path,
349
- // narrowed to vendors that support this step type.
373
+ // 'auto': prefer codex then claude then opencode, same as resolveReviewer's
374
+ // auto path, narrowed to vendors that support this step type.
350
375
  const usable = (v) => config.vendors[v].enabled && supportsStep(v, stepType);
351
- humanFallback = usable('codex') ? 'codex' : usable('claude') ? 'claude' : null;
376
+ humanFallback = usable('codex') ? 'codex' : usable('claude') ? 'claude' : usable('opencode') ? 'opencode' : null;
352
377
  }
353
378
  if (!humanFallback)
354
379
  return { vendor: null, usedHumanFallback: false };
@@ -558,6 +583,7 @@ export function resolveRoundExecution(config, strategy, round) {
558
583
  quality: config.quality,
559
584
  claudeVendor: config.vendors.claude,
560
585
  codexVendor: config.vendors.codex,
586
+ opencodeVendor: config.vendors.opencode,
561
587
  roundConfig: config,
562
588
  escalated: false,
563
589
  };
@@ -575,17 +601,22 @@ export function resolveRoundExecution(config, strategy, round) {
575
601
  if (config.vendors.codex.enabled) {
576
602
  lanes.push({ model: resolveCodexModel(baseQuality, config.vendors.codex), accepted: CODEX_EFFORT_LEVELS });
577
603
  }
604
+ if (config.vendors.opencode.enabled) {
605
+ lanes.push({ model: resolveOpenCodeModel(baseQuality, config.vendors.opencode) ?? 'default', accepted: OPENCODE_EFFORT_LEVELS });
606
+ }
578
607
  const escalated = escalate({ tier: strategy.tier ?? config.quality.tier, effort: strategy.effort }, round, lanes);
579
608
  const roundStrategy = { ...strategy, tier: escalated.tier, effort: escalated.effort };
580
609
  const quality = strategyQuality(config.quality, roundStrategy);
581
610
  const claudeVendor = strategyVendor(config.vendors.claude, roundStrategy, CLAUDE_EFFORT_LEVELS);
582
611
  const codexVendor = strategyVendor(config.vendors.codex, roundStrategy, CODEX_EFFORT_LEVELS);
612
+ const opencodeVendor = strategyVendor(config.vendors.opencode, roundStrategy, OPENCODE_EFFORT_LEVELS);
583
613
  return {
584
614
  strategy: roundStrategy,
585
615
  quality,
586
616
  claudeVendor,
587
617
  codexVendor,
588
- roundConfig: { ...config, quality, vendors: { ...config.vendors, claude: claudeVendor, codex: codexVendor } },
618
+ opencodeVendor,
619
+ roundConfig: { ...config, quality, vendors: { ...config.vendors, claude: claudeVendor, codex: codexVendor, opencode: opencodeVendor } },
589
620
  escalated: escalated.tier !== strategy.tier || escalated.effort !== strategy.effort,
590
621
  };
591
622
  }
@@ -847,7 +878,7 @@ export async function runWorkflow(ctx) {
847
878
  // Above the try, and above emitPRComplexity, because both the complexity event
848
879
  // and workflow_complete report the tier that ran: an escalated round reporting
849
880
  // the base class tier is the same defect as a comment citing one.
850
- const { strategy: roundStrategy, quality, claudeVendor, codexVendor, roundConfig, escalated } = resolveRoundExecution(config, strategy, ctx.round ?? 1);
881
+ const { strategy: roundStrategy, quality, claudeVendor, codexVendor, opencodeVendor, roundConfig, escalated } = resolveRoundExecution(config, strategy, ctx.round ?? 1);
851
882
  emitPRComplexity(ctx, triggerField, quality.tier);
852
883
  try {
853
884
  // Inside the try so a preflight failure still reaches the completion handler in
@@ -869,11 +900,12 @@ export async function runWorkflow(ctx) {
869
900
  const appliedEffort = [...new Set([
870
901
  ...(config.vendors.claude.enabled ? [claudeVendor.effort] : []),
871
902
  ...(config.vendors.codex.enabled ? [codexVendor.effort] : []),
903
+ ...(config.vendors.opencode.enabled ? [opencodeVendor.effort] : []),
872
904
  ])].join('/');
873
905
  const escalatedNote = escalated ? ` · round ${ctx.round} escalated` : '';
874
906
  log(chalk.dim(` strategy v${strategy.version}: ${strategy.classId} → ${roundStrategy.tier ?? 'skip'} tier${appliedEffort ? ` (${appliedEffort})` : ''}${escalatedNote}`));
875
907
  if (escalatedNote) {
876
- fileLog({ level: 'info', event: 'strategy_escalated', repo: `${owner}/${repoName}`, pr: prNumber, round: ctx.round, from_tier: strategy.tier, to_tier: roundStrategy.tier, from_effort: strategy.effort, to_effort: roundStrategy.effort, applied_effort_claude: config.vendors.claude.enabled ? claudeVendor.effort : null, applied_effort_codex: config.vendors.codex.enabled ? codexVendor.effort : null, strategy_version: strategy.version });
908
+ fileLog({ level: 'info', event: 'strategy_escalated', repo: `${owner}/${repoName}`, pr: prNumber, round: ctx.round, from_tier: strategy.tier, to_tier: roundStrategy.tier, from_effort: strategy.effort, to_effort: roundStrategy.effort, applied_effort_claude: config.vendors.claude.enabled ? claudeVendor.effort : null, applied_effort_codex: config.vendors.codex.enabled ? codexVendor.effort : null, applied_effort_opencode: config.vendors.opencode.enabled ? opencodeVendor.effort : null, strategy_version: strategy.version });
877
909
  }
878
910
  }
879
911
  for (const step of steps) {
@@ -978,6 +1010,12 @@ export async function runWorkflow(ctx) {
978
1010
  inputTokens = undefined;
979
1011
  outputTokens = undefined;
980
1012
  }
1013
+ else if (candidate === 'opencode') {
1014
+ ;
1015
+ ({ review: rawReview, tokensUsed, model, effort, retried } = await runOpenCodeReview(tmpDir, pr.base.ref, pr.title, quality, opencodeVendor, memoryPlan?.instructions ?? step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.opencode.timeout_sec), log, reviewContext, skillSession));
1016
+ inputTokens = undefined;
1017
+ outputTokens = undefined;
1018
+ }
981
1019
  else {
982
1020
  ;
983
1021
  ({ review: rawReview, tokensUsed, inputTokens, outputTokens, model, effort, retried } = await runClaudeReview(tmpDir, pr.base.ref, pr.title, quality, claudeVendor, config.budget.per_review_usd, memoryPlan?.instructions ?? step.instructions, undefined, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec), !!ctx.roundMode, log, reviewContext, skillSession));
@@ -1160,7 +1198,7 @@ export async function runWorkflow(ctx) {
1160
1198
  // Withheld when an explicit vendors.*.model overrode the tier map:
1161
1199
  // citing a tier the run did not use would assert a routing decision
1162
1200
  // that never happened.
1163
- strategyCitation(reviewer === 'codex' ? config.vendors.codex : config.vendors.claude, strategy, roundStrategy, model));
1201
+ strategyCitation(reviewer === 'codex' ? config.vendors.codex : reviewer === 'opencode' ? config.vendors.opencode : config.vendors.claude, strategy, roundStrategy, model));
1164
1202
  if (memoryPlan && structured?.snapshot) {
1165
1203
  // The review is already posted; a memory write failure only costs the next review its delta.
1166
1204
  try {
@@ -1313,6 +1351,7 @@ export async function runWorkflow(ctx) {
1313
1351
  // away.
1314
1352
  const claudeFixModel = resolveClaudeModel(quality, claudeVendor);
1315
1353
  const codexFixModel = resolveCodexModel(quality, codexVendor);
1354
+ const opencodeFixModel = resolveOpenCodeModel(quality, opencodeVendor);
1316
1355
  // Guard: don't push more than MAX_CROSSCHECK_COMMITS per PR.
1317
1356
  // Scope to commits ahead of base so long-lived branches (e.g. staging)
1318
1357
  // don't count [crosscheck] commits from previously merged PRs.
@@ -1345,6 +1384,9 @@ export async function runWorkflow(ctx) {
1345
1384
  if (v === 'codex') {
1346
1385
  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);
1347
1386
  }
1387
+ if (v === 'opencode') {
1388
+ return runOpenCodeFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', opencodeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.opencode.timeout_sec) ?? tierMs, skillSession, opencodeVendor.effort, fixHumanFeedback);
1389
+ }
1348
1390
  // roundConfig, not config: runFixStep reads vendors.claude.effort and
1349
1391
  // quality.tier out of it, and both must be this round's values.
1350
1392
  return runFixStep(tmpDir, pr.base.ref, pr.title, reviewCommentBody, step.instructions ?? '', roundConfig, claudeFixModel, ctx.overrideTimeoutMs ?? vendorTimeoutMs(config.vendors.claude.timeout_sec) ?? tierMs, skillSession, fixHumanFeedback);
@@ -1411,7 +1453,7 @@ export async function runWorkflow(ctx) {
1411
1453
  const fixAttributionFooter = () => buildAttributionFooter({
1412
1454
  action: 'Fixed',
1413
1455
  vendor: activeVendor,
1414
- model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
1456
+ model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1415
1457
  effort: fixEffort,
1416
1458
  skills: activatedSkills,
1417
1459
  });
@@ -1426,7 +1468,7 @@ export async function runWorkflow(ctx) {
1426
1468
  body: buildFixFailedCommentBody({
1427
1469
  prUrl: pr.html_url,
1428
1470
  vendor: activeVendor,
1429
- model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
1471
+ model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1430
1472
  effort: fixEffort,
1431
1473
  skills: activatedSkills,
1432
1474
  }),
@@ -1460,7 +1502,7 @@ export async function runWorkflow(ctx) {
1460
1502
  const deliveryMode = config.post_review.auto_fix.delivery.mode;
1461
1503
  const landing = resolveFixLanding(deliveryMode);
1462
1504
  if (landing === 'branch') {
1463
- const fixModel = activeVendor === 'codex' ? codexFixModel : claudeFixModel;
1505
+ const fixModel = activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? (opencodeFixModel ?? 'default') : claudeFixModel;
1464
1506
  runGitWithoutHooks(tmpDir, ['add', '-A']);
1465
1507
  runGitWithoutHooks(tmpDir, [
1466
1508
  'commit',
@@ -1512,7 +1554,7 @@ export async function runWorkflow(ctx) {
1512
1554
  changedFiles: fixChangedFiles,
1513
1555
  vendor: activeVendor,
1514
1556
  reviewCommentBody,
1515
- model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
1557
+ model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1516
1558
  effort: fixEffort,
1517
1559
  skills: activatedSkills,
1518
1560
  });
@@ -1525,7 +1567,7 @@ export async function runWorkflow(ctx) {
1525
1567
  results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
1526
1568
  }
1527
1569
  else if (landing === 'branch-then-separate-pr') {
1528
- const fixModel = activeVendor === 'codex' ? codexFixModel : claudeFixModel;
1570
+ const fixModel = activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? (opencodeFixModel ?? 'default') : claudeFixModel;
1529
1571
  // Commit the fix on the PR's own branch (already checked out in tmpDir) and
1530
1572
  // try to push it there, so the fix, recheck, and approval all stay on the
1531
1573
  // original PR. Only when that push can't land — e.g. the PR was merged and
@@ -1585,7 +1627,7 @@ export async function runWorkflow(ctx) {
1585
1627
  changedFiles: fixChangedFiles,
1586
1628
  vendor: activeVendor,
1587
1629
  reviewCommentBody,
1588
- model: activeVendor === 'codex' ? codexFixModel : claudeFixModel,
1630
+ model: activeVendor === 'codex' ? codexFixModel : activeVendor === 'opencode' ? opencodeFixModel : claudeFixModel,
1589
1631
  effort: fixEffort,
1590
1632
  skills: activatedSkills,
1591
1633
  });
@@ -1920,6 +1962,15 @@ export async function runWorkflow(ctx) {
1920
1962
  skipConflictResolve('codex_conflict_resolve_unsupported');
1921
1963
  continue;
1922
1964
  }
1965
+ if (vendor === 'opencode') {
1966
+ try {
1967
+ runGitWithoutHooks(tmpDir, ['merge', '--abort']);
1968
+ }
1969
+ catch { /* ignore */ }
1970
+ ;
1971
+ skipConflictResolve('opencode_conflict_resolve_unsupported');
1972
+ continue;
1973
+ }
1923
1974
  // Conflict-resolve is mechanical text surgery bounded by the markers —
1924
1975
  // measured at 37s against ~643s for a review — so it always runs fast.
1925
1976
  const conflictResolveModel = resolveClaudeModel({ ...config.quality, tier: config.quality.mode === 'smart' ? 'fast' : config.quality.tier }, config.vendors.claude);