@link-assistant/hive-mind 1.36.1 → 1.37.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.37.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f02c1fc: fix synthetic model appearing in PR comments by filtering internal Claude CLI router entries (Issue #1486)
8
+ - dd87b23: Add opusplan model support and --plan-model option for flexible plan/execution model pairing
9
+
3
10
  ## 1.36.1
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -859,6 +859,12 @@ ps axjf
859
859
  pkill -f gh-issue-solver-1773073065743
860
860
  ```
861
861
 
862
+ ### Kill all headless browsers spawned by ms-playwright
863
+
864
+ ```bash
865
+ pkill -f ms-playwright/chromium_headless_shell-1200
866
+ ```
867
+
862
868
  That can be done, but not recommended as reboot have better effect.
863
869
 
864
870
  ## šŸ“„ License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.36.1",
3
+ "version": "1.37.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -586,13 +586,13 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
586
586
  // Read the entire file
587
587
  const fileContent = await fs.readFile(sessionFile, 'utf8');
588
588
  const lines = fileContent.trim().split('\n');
589
- // Parse each line and accumulate token counts per model
590
589
  for (const line of lines) {
591
590
  if (!line.trim()) continue;
592
591
  try {
593
592
  const entry = JSON.parse(line);
594
593
  if (entry.message && entry.message.usage && entry.message.model) {
595
594
  const model = entry.message.model;
595
+ if (model.startsWith('<') && model.endsWith('>')) continue; // Issue #1486: skip <synthetic> etc.
596
596
  const usage = entry.message.usage;
597
597
  // Initialize model entry if it doesn't exist
598
598
  if (!modelUsage[model]) {
@@ -842,11 +842,12 @@ export const executeClaudeCommand = async params => {
842
842
  } else if (argv.interactiveMode) {
843
843
  await log('āš ļø Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
844
844
  }
845
- // Build claude command with optional resume flag
846
845
  let execCommand;
847
846
  const mappedModel = mapModelToId(argv.model);
848
- // Build claude command arguments
849
- let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel}`;
847
+ const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
848
+ const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
849
+ const resolvedExecutionModel = resolvedPlanModel ? mappedModel : undefined;
850
+ let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
850
851
  if (argv.resume) {
851
852
  await log(`šŸ”„ Resuming from session: ${argv.resume}`);
852
853
  claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
@@ -862,22 +863,22 @@ export const executeClaudeCommand = async params => {
862
863
  }
863
864
  try {
864
865
  const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
865
- const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: mappedModel, thinkLevel, maxBudget });
866
+ const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel });
866
867
  if (argv.verbose) claudeEnv.ANTHROPIC_LOG = 'debug';
867
- const modelMaxOutputTokens = getMaxOutputTokensForModel(mappedModel);
868
+ const modelMaxOutputTokens = getMaxOutputTokensForModel(effectiveModel);
868
869
  if (argv.verbose) {
869
870
  await log(`šŸ“Š CLAUDE_CODE_MAX_OUTPUT_TOKENS: ${modelMaxOutputTokens}, MCP_TIMEOUT: ${claudeCode.mcpTimeout}ms, MCP_TOOL_TIMEOUT: ${claudeCode.mcpToolTimeout}ms, ANTHROPIC_LOG: debug`, { verbose: true });
871
+ if (resolvedPlanModel) await log(`šŸ“Š opusplan: plan=${resolvedPlanModel}, exec=${resolvedExecutionModel}`, { verbose: true });
870
872
  if (resolvedThinkingBudget !== undefined) await log(`šŸ“Š MAX_THINKING_TOKENS: ${resolvedThinkingBudget}`, { verbose: true });
871
873
  if (claudeEnv.CLAUDE_CODE_EFFORT_LEVEL) await log(`šŸ“Š CLAUDE_CODE_EFFORT_LEVEL: ${claudeEnv.CLAUDE_CODE_EFFORT_LEVEL}`, { verbose: true });
872
874
  if (!isNewVersion && thinkLevel) await log(`šŸ“Š Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
873
875
  }
876
+ const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
874
877
  if (argv.resume) {
875
878
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
876
- const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
877
- execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
879
+ execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
878
880
  } else {
879
- const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
880
- execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${mappedModel} --append-system-prompt "${simpleEscapedSystem}"`;
881
+ execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} --append-system-prompt "${simpleEscapedSystem}"`;
881
882
  }
882
883
  await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`);
883
884
  await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2));
@@ -1262,7 +1263,6 @@ export const executeClaudeCommand = async params => {
1262
1263
  sessionId,
1263
1264
  resumeCommand: argv.url ? `${process.argv[0]} ${process.argv[1]} --auto-continue ${argv.url}` : null,
1264
1265
  });
1265
-
1266
1266
  for (const line of messageLines) {
1267
1267
  await log(line, { level: 'warning' });
1268
1268
  }
@@ -174,9 +174,10 @@ export const DEFAULT_MAX_THINKING_BUDGET_OPUS_46 = parseIntWithDefault('HIVE_MIN
174
174
  export const isOpus46OrLater = model => {
175
175
  if (!model) return false;
176
176
  const normalizedModel = model.toLowerCase();
177
- // Check for explicit opus-4-6 or later versions
177
+ // Check for explicit opus-4-6 or later versions, or opusplan (Issue #1223)
178
178
  // Note: The 'opus' alias now maps to Opus 4.6 (Issue #1433), so we also check for the alias directly
179
- return normalizedModel === 'opus' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-5');
179
+ // opusplan uses Opus for planning, so it should get Opus-level settings
180
+ return normalizedModel === 'opus' || normalizedModel === 'opusplan' || normalizedModel.includes('opus-4-6') || normalizedModel.includes('opus-4-7') || normalizedModel.includes('opus-5');
180
181
  };
181
182
 
182
183
  /**
@@ -318,6 +319,10 @@ export const supportsThinkingBudget = (version, minVersion = '2.1.12') => {
318
319
  // Also sets MCP_TIMEOUT and MCP_TOOL_TIMEOUT for MCP tool execution (see issue #1066)
319
320
  // Supports model-specific max output tokens for Opus 4.6 (Issue #1221)
320
321
  // Sets CLAUDE_CODE_EFFORT_LEVEL for Opus 4.6 models (Issue #1238)
322
+ // Supports planModel/executionModel for opusplan mode (Issue #1223)
323
+ // See: https://code.claude.com/docs/en/model-config
324
+ // ANTHROPIC_DEFAULT_OPUS_MODEL → model used in plan mode (and for 'opus' alias)
325
+ // ANTHROPIC_DEFAULT_SONNET_MODEL → model used in execution mode (and for 'sonnet' alias)
321
326
  export const getClaudeEnv = (options = {}) => {
322
327
  // Get max output tokens based on model (Issue #1221)
323
328
  const maxOutputTokens = options.model ? getMaxOutputTokensForModel(options.model) : claudeCode.maxOutputTokens;
@@ -327,8 +332,6 @@ export const getClaudeEnv = (options = {}) => {
327
332
  CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(maxOutputTokens),
328
333
  // MCP timeout configurations to prevent tool calls from hanging indefinitely
329
334
  // See: https://github.com/link-assistant/hive-mind/issues/1066
330
- // MCP_TIMEOUT: Timeout for MCP server startup
331
- // MCP_TOOL_TIMEOUT: Timeout for MCP tool execution (the one that prevents stuck tools)
332
335
  MCP_TIMEOUT: String(claudeCode.mcpTimeout),
333
336
  MCP_TOOL_TIMEOUT: String(claudeCode.mcpToolTimeout),
334
337
  };
@@ -354,6 +357,17 @@ export const getClaudeEnv = (options = {}) => {
354
357
  env.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
355
358
  }
356
359
  }
360
+ // Set ANTHROPIC_DEFAULT_OPUS_MODEL when planModel is specified (Issue #1223)
361
+ // This tells Claude Code which model to use during plan mode in opusplan
362
+ if (options.planModel) {
363
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = String(options.planModel);
364
+ }
365
+ // Set ANTHROPIC_DEFAULT_SONNET_MODEL when executionModel is specified (Issue #1223)
366
+ // This tells Claude Code which model to use during execution mode in opusplan
367
+ // Enables combinations like --plan-model opus --model haiku
368
+ if (options.executionModel) {
369
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = String(options.executionModel);
370
+ }
357
371
 
358
372
  return env;
359
373
  };
@@ -413,6 +427,7 @@ const defaultAvailableModels = `(
413
427
  opus
414
428
  sonnet
415
429
  haiku
430
+ opusplan
416
431
  )`;
417
432
 
418
433
  export const modelConfig = {
@@ -390,9 +390,7 @@ export async function attachLogToGitHub(options) {
390
390
  if (useLargeFileMode && verbose) {
391
391
  await log(` šŸ“ Large log file (${Math.round(logStats.size / 1024 / 1024)}MB), will use gh-upload-log`, { verbose: true });
392
392
  }
393
- // Calculate token usage if sessionId and tempDir are provided (skip for agent tool with pricing)
394
- let totalCostUSD = publicPricingEstimate;
395
- // Issue #1225: Collect actual model IDs from Claude session JSON output
393
+ let totalCostUSD = publicPricingEstimate; // Issue #1225: token usage + actual model IDs
396
394
  let actualModelIds = null;
397
395
  if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
398
396
  try {
@@ -401,23 +399,15 @@ export async function attachLogToGitHub(options) {
401
399
  if (tokenUsage) {
402
400
  if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
403
401
  totalCostUSD = tokenUsage.totalCostUSD;
404
- if (verbose) {
405
- await log(` šŸ’° Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
406
- }
402
+ if (verbose) await log(` šŸ’° Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
407
403
  }
408
- // Extract actual model IDs from session data (Issue #1225)
409
404
  if (tokenUsage.modelUsage && Object.keys(tokenUsage.modelUsage).length > 0) {
410
405
  actualModelIds = Object.keys(tokenUsage.modelUsage);
411
- if (verbose) {
412
- await log(` šŸ¤– Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
413
- }
406
+ if (verbose) await log(` šŸ¤– Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
414
407
  }
415
408
  }
416
409
  } catch (tokenError) {
417
- // Don't fail the entire upload if token calculation fails
418
- if (verbose) {
419
- await log(` āš ļø Could not calculate token cost: ${tokenError.message}`, { verbose: true });
420
- }
410
+ if (verbose) await log(` āš ļø Could not calculate token cost: ${tokenError.message}`, { verbose: true });
421
411
  }
422
412
  }
423
413
  // Issue #1454: Use resultModelUsage from result JSON when it has more models (includes subagent models)
@@ -433,6 +423,11 @@ export async function attachLogToGitHub(options) {
433
423
  if (!actualModelIds && pricingInfo?.modelId) {
434
424
  actualModelIds = [pricingInfo.modelId];
435
425
  }
426
+ // Issue #1486: Filter out internal/synthetic model entries (e.g., "<synthetic>" from Claude CLI's inference router)
427
+ if (actualModelIds) {
428
+ actualModelIds = actualModelIds.filter(id => !(id.startsWith('<') && id.endsWith('>')));
429
+ if (actualModelIds.length === 0) actualModelIds = null;
430
+ }
436
431
  // Issue #1225: Fetch model information for comment using actual models from CLI output
437
432
  let modelInfoString = '';
438
433
  if (requestedModel || tool || actualModelIds) {
@@ -21,7 +21,7 @@ const HIVE_CUSTOM_SOLVE_OPTIONS = {
21
21
  model: {
22
22
  type: 'string',
23
23
  description: `${buildModelOptionDescription()}, or any model ID supported by the tool`,
24
- alias: 'm',
24
+ alias: ['m', 'worker-model'],
25
25
  default: 'sonnet',
26
26
  },
27
27
  'dry-run': {
package/src/hive.mjs CHANGED
@@ -152,9 +152,7 @@ if (isDirectExecution) {
152
152
  // Strategy 2: Fallback to gh api --paginate approach (comprehensive but slower)
153
153
  await log(' šŸ“‹ Using gh api --paginate approach for comprehensive coverage...', { verbose: true });
154
154
 
155
- // First, get list of ALL repositories using gh api with --paginate for unlimited pagination
156
- // This approach uses the GitHub API directly to fetch all repositories without any limits
157
- // Include isArchived field to filter out archived repositories
155
+ // Get list of ALL repositories using gh api with --paginate (includes isArchived for filtering)
158
156
  let repoListCmd;
159
157
  if (scope === 'organization') {
160
158
  repoListCmd = `gh api orgs/${owner}/repos --paginate --jq '.[] | {name: .name, owner: .owner.login, isArchived: .archived}'`;
@@ -436,8 +434,6 @@ if (isDirectExecution) {
436
434
  initializeExitHandler(getAbsoluteLogPath, log);
437
435
  installGlobalExitHandlers();
438
436
 
439
- // Unhandled error handlers are now managed by exit-handler.lib.mjs
440
-
441
437
  // Validate GitHub URL requirement
442
438
  if (!githubUrl) {
443
439
  await log('āŒ GitHub URL is required', { level: 'error' });
@@ -470,18 +466,28 @@ if (isDirectExecution) {
470
466
  }
471
467
  }
472
468
 
473
- // Validate model name EARLY - this always runs regardless of --skip-tool-connection-check
474
- // Model validation is a simple string check and should always be performed
469
+ // --plan flag expansion: shortcut for --plan-model opus --worker-model sonnet (Issue #1223)
470
+ if (argv.plan) {
471
+ if (!rawArgs.includes('--plan-model')) argv.planModel = 'opus';
472
+ if (!rawArgs.includes('--model') && !rawArgs.includes('-m') && !rawArgs.includes('--worker-model')) argv.model = 'sonnet';
473
+ }
474
+
475
+ // Validate model names EARLY (simple string check, always runs)
475
476
  const tool = argv.tool || 'claude';
476
477
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
478
+ if (argv.planModel) {
479
+ if (tool !== 'claude') {
480
+ await log(`āŒ --plan-model is only supported with --tool claude (current tool: ${tool})`, { level: 'error' });
481
+ await safeExit(1, '--plan-model requires --tool claude');
482
+ }
483
+ await validateAndExitOnInvalidModel(argv.planModel, tool, safeExit);
484
+ }
477
485
 
478
486
  // Handle -s (--skip-issues-with-prs) and --auto-continue interaction
479
- // Detect if user explicitly passed --auto-continue or --no-auto-continue
480
487
  const hasExplicitAutoContinue = rawArgs.includes('--auto-continue');
481
488
  const hasExplicitNoAutoContinue = rawArgs.includes('--no-auto-continue');
482
489
 
483
490
  if (argv.skipIssuesWithPrs) {
484
- // If user explicitly passed --auto-continue with -s, that's a conflict
485
491
  if (hasExplicitAutoContinue) {
486
492
  await log('āŒ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', {
487
493
  level: 'error',
@@ -492,8 +498,7 @@ if (isDirectExecution) {
492
498
  await safeExit(1, 'Error occurred');
493
499
  }
494
500
 
495
- // If user didn't explicitly set auto-continue, disable it when -s is used
496
- // This is because -s means "skip issues with PRs" which conflicts with auto-continue
501
+ // -s implies disabling auto-continue unless explicitly set
497
502
  if (!hasExplicitNoAutoContinue) {
498
503
  argv.autoContinue = false;
499
504
  }
@@ -778,10 +783,8 @@ if (isDirectExecution) {
778
783
  }
779
784
  if (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) args.push('--skip-tool-connection-check');
780
785
  if (argv.dryRun) args.push('--dry-run');
781
- if (argv.autoCleanup) args.push('--auto-cleanup'); // hive default differs from solve's auto-detect default
782
-
783
- // Options already handled above or deprecated aliases (skip in generic loop)
784
- const SKIP_AUTO_FORWARD = new Set(['model', 'base-branch', 'skip-tool-connection-check', 'tool-connection-check', 'skip-tool-check', 'skip-claude-check', 'tool-check', 'dry-run', 'auto-cleanup']);
786
+ if (argv.autoCleanup) args.push('--auto-cleanup');
787
+ const SKIP_AUTO_FORWARD = new Set(['model', 'worker-model', 'base-branch', 'skip-tool-connection-check', 'tool-connection-check', 'skip-tool-check', 'skip-claude-check', 'tool-check', 'dry-run', 'auto-cleanup']);
785
788
 
786
789
  for (const optionName of getSolvePassthroughOptionNames()) {
787
790
  if (SKIP_AUTO_FORWARD.has(optionName)) continue;
@@ -1431,8 +1434,7 @@ if (isDirectExecution) {
1431
1434
  await safeExit(0, 'Process completed');
1432
1435
  }
1433
1436
 
1434
- // Function to validate Claude CLI connection
1435
- // validateClaudeConnection is now imported from lib.mjs
1437
+ // validateClaudeConnection is imported from lib.mjs
1436
1438
 
1437
1439
  // Handle graceful shutdown
1438
1440
  process.on('SIGINT', () => gracefulShutdown('interrupt'));
@@ -1484,8 +1486,7 @@ if (isDirectExecution) {
1484
1486
  await safeExit(1, 'Error occurred');
1485
1487
  }
1486
1488
  } catch (fatalError) {
1487
- // Handle any errors that occurred during initialization or execution
1488
- // This prevents silent failures when the script hangs or crashes
1489
+ // Handle fatal errors during initialization or execution
1489
1490
  console.error('\nāŒ Fatal error occurred during hive initialization or execution');
1490
1491
  console.error(` ${fatalError.message || fatalError}`);
1491
1492
  if (fatalError.stack) {
@@ -30,6 +30,7 @@ export const claudeModels = {
30
30
  haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
31
31
  'haiku-3-5': 'claude-3-5-haiku-20241022', // Haiku 3.5
32
32
  'haiku-3': 'claude-3-haiku-20240307', // Haiku 3
33
+ opusplan: 'opusplan', // Special mode: Opus for planning, Sonnet for execution (Issue #1223)
33
34
  // Shorter version aliases (Issue #1221, Issue #1329 - PR comment feedback)
34
35
  'sonnet-4-6': 'claude-sonnet-4-6', // Sonnet 4.6 short alias (Issue #1329)
35
36
  'opus-4-6': 'claude-opus-4-6', // Opus 4.6 short alias
@@ -258,7 +259,7 @@ export const isModelCompatibleWithTool = (tool, model) => {
258
259
 
259
260
  switch (tool) {
260
261
  case 'claude':
261
- return mappedModel.startsWith('claude-');
262
+ return mappedModel.startsWith('claude-') || mappedModel === 'opusplan';
262
263
  case 'agent':
263
264
  return mappedModel.includes('/') || Object.keys(agentModels).includes(model);
264
265
  case 'opencode':
@@ -293,7 +294,7 @@ export const getValidModelsForTool = tool => {
293
294
  // Primary (non-alias, non-deprecated) short names shown in CLI help descriptions
294
295
  // These are the recommended model names users should see in --model help text
295
296
  export const primaryModelNames = {
296
- claude: ['opus', 'sonnet', 'haiku'],
297
+ claude: ['opus', 'sonnet', 'haiku', 'opusplan'],
297
298
  opencode: ['grok', 'gpt4o'],
298
299
  codex: ['gpt5', 'gpt5-codex', 'o3'],
299
300
  agent: ['minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free'],
@@ -284,6 +284,21 @@ export const SOLVE_OPTION_DEFINITIONS = {
284
284
  choices: ['claude', 'opencode', 'codex', 'agent'],
285
285
  default: 'claude',
286
286
  },
287
+ plan: {
288
+ type: 'boolean',
289
+ description: 'Enable plan mode: uses opus for planning, sonnet for execution (shortcut for --plan-model opus --worker-model sonnet). Only works with --tool claude.',
290
+ default: false,
291
+ },
292
+ 'plan-model': {
293
+ type: 'string',
294
+ description: 'Model to use for plan mode (e.g., opus). When specified, auto-switches to opusplan mode and sets ANTHROPIC_DEFAULT_OPUS_MODEL. Use with --model/--worker-model to set separate plan and execution models (e.g., --plan-model opus --model sonnet). Only works with --tool claude.',
295
+ default: undefined,
296
+ },
297
+ 'worker-model': {
298
+ type: 'string',
299
+ description: 'Alias for --model: Model to use for execution/worker mode when --plan-model is specified. When used with --plan-model, sets ANTHROPIC_DEFAULT_SONNET_MODEL for Claude Code opusplan mode.',
300
+ default: undefined,
301
+ },
287
302
  'execute-tool-with-bun': {
288
303
  type: 'boolean',
289
304
  description: 'Execute the AI tool using bunx (experimental, may improve speed and memory usage)',
@@ -441,7 +456,7 @@ export const createYargsConfig = yargsInstance => {
441
456
  .option('model', {
442
457
  type: 'string',
443
458
  description: buildModelOptionDescription(),
444
- alias: 'm',
459
+ alias: ['m', 'worker-model'],
445
460
  default: currentParsedArgs => {
446
461
  // Dynamic default based on tool selection (Issue #1473: centralized in models/index.mjs)
447
462
  return defaultModels[currentParsedArgs?.tool] || defaultModels.claude;
@@ -549,7 +564,20 @@ export const parseArguments = async (yargs, hideBin) => {
549
564
  // Post-processing: Fix model default for opencode and codex tools
550
565
  // Yargs doesn't properly handle dynamic defaults based on other arguments,
551
566
  // so we need to handle this manually after parsing
552
- const modelExplicitlyProvided = rawArgs.includes('--model') || rawArgs.includes('-m');
567
+ const modelExplicitlyProvided = rawArgs.includes('--model') || rawArgs.includes('-m') || rawArgs.includes('--worker-model');
568
+ const planModelExplicitlyProvided = rawArgs.includes('--plan-model');
569
+
570
+ // --plan flag expansion (Issue #1223)
571
+ // When --plan is set, it acts as a shortcut for --plan-model opus --worker-model sonnet
572
+ // Explicit --plan-model and --model/--worker-model values take precedence
573
+ if (argv && argv.plan) {
574
+ if (!planModelExplicitlyProvided) {
575
+ argv.planModel = 'opus';
576
+ }
577
+ if (!modelExplicitlyProvided) {
578
+ argv.model = 'sonnet';
579
+ }
580
+ }
553
581
 
554
582
  // Normalize alias flags: legacy --skip-tool-check and --skip-claude-check behave like --skip-tool-connection-check
555
583
  if (argv) {
package/src/solve.mjs CHANGED
@@ -211,6 +211,15 @@ if (!(await validateContinueOnlyOnFeedback(argv, isPrUrl, isIssueUrl))) {
211
211
  const tool = argv.tool || 'claude';
212
212
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
213
213
 
214
+ // Validate --plan-model if provided (Issue #1223)
215
+ if (argv.planModel) {
216
+ if (tool !== 'claude') {
217
+ await log(`āŒ --plan-model is only supported with --tool claude (current tool: ${tool})`, { level: 'error' });
218
+ await safeExit(1, '--plan-model requires --tool claude');
219
+ }
220
+ await validateAndExitOnInvalidModel(argv.planModel, tool, safeExit);
221
+ }
222
+
214
223
  // Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
215
224
  const skipToolConnectionCheck = argv.dryRun || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
216
225
  if (!(await performSystemChecks(argv.minDiskSpace || 2048, skipToolConnectionCheck, argv.model, argv))) {