@link-assistant/hive-mind 1.36.0 → 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 +13 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +11 -11
- package/src/config.lib.mjs +19 -4
- package/src/github-merge.lib.mjs +168 -0
- package/src/github.lib.mjs +9 -14
- package/src/hive.config.lib.mjs +1 -1
- package/src/hive.mjs +20 -19
- package/src/models/index.mjs +3 -2
- package/src/solve.auto-merge.lib.mjs +80 -8
- package/src/solve.config.lib.mjs +30 -2
- package/src/solve.mjs +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
10
|
+
## 1.36.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 74bf211: fix false positive 'Ready to merge' by adding workflow run grace period (Issue #1480)
|
|
15
|
+
|
|
3
16
|
## 1.36.0
|
|
4
17
|
|
|
5
18
|
### Minor 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
package/src/claude.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
849
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/config.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
package/src/github-merge.lib.mjs
CHANGED
|
@@ -1286,6 +1286,170 @@ export async function getActiveRepoWorkflows(owner, repo, verbose = false) {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
}
|
|
1288
1288
|
|
|
1289
|
+
/**
|
|
1290
|
+
* Get the committed date of a specific commit from GitHub API
|
|
1291
|
+
* Issue #1480: Used to determine how recently a commit was pushed, to distinguish between
|
|
1292
|
+
* "CI not yet registered in API" (race condition) and "CI definitively not triggered"
|
|
1293
|
+
* @param {string} owner - Repository owner
|
|
1294
|
+
* @param {string} repo - Repository name
|
|
1295
|
+
* @param {string} sha - Commit SHA
|
|
1296
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1297
|
+
* @returns {Promise<{date: Date|null, ageSeconds: number|null}>}
|
|
1298
|
+
*/
|
|
1299
|
+
export async function getCommitDate(owner, repo, sha, verbose = false) {
|
|
1300
|
+
try {
|
|
1301
|
+
const { stdout } = await exec(`gh api repos/${owner}/${repo}/commits/${sha} --jq '.commit.committer.date'`);
|
|
1302
|
+
const dateStr = stdout.trim();
|
|
1303
|
+
if (!dateStr) {
|
|
1304
|
+
return { date: null, ageSeconds: null };
|
|
1305
|
+
}
|
|
1306
|
+
const commitDate = new Date(dateStr);
|
|
1307
|
+
const ageSeconds = Math.floor((Date.now() - commitDate.getTime()) / 1000);
|
|
1308
|
+
if (verbose) {
|
|
1309
|
+
console.log(`[VERBOSE] /merge: Commit ${sha.substring(0, 7)} date: ${dateStr} (${ageSeconds}s ago)`);
|
|
1310
|
+
}
|
|
1311
|
+
return { date: commitDate, ageSeconds };
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
if (verbose) {
|
|
1314
|
+
console.log(`[VERBOSE] /merge: Error fetching commit date for ${sha}: ${error.message}`);
|
|
1315
|
+
}
|
|
1316
|
+
return { date: null, ageSeconds: null };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Check if any previous commits in a PR had workflow runs triggered.
|
|
1322
|
+
* Issue #1480: If earlier commits in the same PR triggered CI, we should expect CI
|
|
1323
|
+
* for the HEAD commit too (unless conditions changed). This provides an additional
|
|
1324
|
+
* signal that CI should be expected and avoids false "CI not triggered" conclusions.
|
|
1325
|
+
* @param {string} owner - Repository owner
|
|
1326
|
+
* @param {string} repo - Repository name
|
|
1327
|
+
* @param {number} prNumber - Pull request number
|
|
1328
|
+
* @param {string} headSha - Current HEAD SHA (to exclude from check)
|
|
1329
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1330
|
+
* @returns {Promise<{hadPreviousCI: boolean, previousCommitsWithCI: number, totalPreviousCommits: number}>}
|
|
1331
|
+
*/
|
|
1332
|
+
export async function checkPreviousPRCommitsHadCI(owner, repo, prNumber, headSha, verbose = false) {
|
|
1333
|
+
try {
|
|
1334
|
+
// Get all commits in the PR
|
|
1335
|
+
const { stdout: commitsJson } = await exec(`gh api "repos/${owner}/${repo}/pulls/${prNumber}/commits?per_page=100" --jq '[.[].sha]'`);
|
|
1336
|
+
const allShas = JSON.parse(commitsJson.trim() || '[]');
|
|
1337
|
+
|
|
1338
|
+
// Exclude the current HEAD SHA
|
|
1339
|
+
const previousShas = allShas.filter(sha => sha !== headSha);
|
|
1340
|
+
|
|
1341
|
+
if (previousShas.length === 0) {
|
|
1342
|
+
if (verbose) {
|
|
1343
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no previous commits to check for CI history`);
|
|
1344
|
+
}
|
|
1345
|
+
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Check the most recent previous commits (limit to last 3 to avoid excessive API calls)
|
|
1349
|
+
const commitsToCheck = previousShas.slice(-3);
|
|
1350
|
+
let commitsWithCI = 0;
|
|
1351
|
+
|
|
1352
|
+
for (const sha of commitsToCheck) {
|
|
1353
|
+
try {
|
|
1354
|
+
const { stdout } = await exec(`gh api "repos/${owner}/${repo}/actions/runs?head_sha=${sha}&per_page=1" --jq '.total_count'`);
|
|
1355
|
+
const count = parseInt(stdout.trim(), 10);
|
|
1356
|
+
if (count > 0) {
|
|
1357
|
+
commitsWithCI++;
|
|
1358
|
+
}
|
|
1359
|
+
} catch {
|
|
1360
|
+
// Skip errors for individual commits
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const hadPreviousCI = commitsWithCI > 0;
|
|
1365
|
+
|
|
1366
|
+
if (verbose) {
|
|
1367
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} previous CI history: ${commitsWithCI}/${commitsToCheck.length} checked commits had workflow runs (total PR commits: ${allShas.length})`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return { hadPreviousCI, previousCommitsWithCI: commitsWithCI, totalPreviousCommits: previousShas.length };
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
if (verbose) {
|
|
1373
|
+
console.log(`[VERBOSE] /merge: Error checking previous PR commits CI history: ${error.message}`);
|
|
1374
|
+
}
|
|
1375
|
+
return { hadPreviousCI: false, previousCommitsWithCI: 0, totalPreviousCommits: 0 };
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Check if any workflow files in the repository have PR-related triggers
|
|
1381
|
+
* Issue #1480: Used as additional signal to determine if CI should run on PRs.
|
|
1382
|
+
* Parses .github/workflows/*.yml files from the repository content API.
|
|
1383
|
+
* @param {string} owner - Repository owner
|
|
1384
|
+
* @param {string} repo - Repository name
|
|
1385
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
1386
|
+
* @returns {Promise<{hasPRTriggers: boolean, hasWorkflowFiles: boolean, workflows: Array<{name: string, triggers: string[]}>}>}
|
|
1387
|
+
*/
|
|
1388
|
+
export async function checkWorkflowsHavePRTriggers(owner, repo, verbose = false) {
|
|
1389
|
+
try {
|
|
1390
|
+
// List workflow files in .github/workflows/
|
|
1391
|
+
const { stdout: listJson } = await exec(`gh api "repos/${owner}/${repo}/contents/.github/workflows" --jq '[.[] | select(.name | test("\\\\.(yml|yaml)$")) | {name: .name, download_url: .download_url, path: .path}]' 2>/dev/null`);
|
|
1392
|
+
const files = JSON.parse(listJson.trim() || '[]');
|
|
1393
|
+
|
|
1394
|
+
if (files.length === 0) {
|
|
1395
|
+
if (verbose) {
|
|
1396
|
+
console.log(`[VERBOSE] /merge: No workflow files found in ${owner}/${repo}/.github/workflows/ — no CI/CD will execute`);
|
|
1397
|
+
}
|
|
1398
|
+
// Issue #1480: hasWorkflowFiles=false is a strong signal that no CI/CD is configured at the file level
|
|
1399
|
+
return { hasPRTriggers: false, hasWorkflowFiles: false, workflows: [] };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const prTriggerPatterns = [/\bon:\s*\n\s+pull_request/m, /\bon:\s*\[.*pull_request.*\]/m, /\bon:\s*pull_request\b/m, /\bpull_request_target\b/m];
|
|
1403
|
+
|
|
1404
|
+
// Also check for push triggers (push to PR branches triggers CI)
|
|
1405
|
+
const pushTriggerPatterns = [/\bon:\s*\n\s+push/m, /\bon:\s*\[.*push.*\]/m, /\bon:\s*push\b/m];
|
|
1406
|
+
|
|
1407
|
+
const results = [];
|
|
1408
|
+
|
|
1409
|
+
for (const file of files) {
|
|
1410
|
+
try {
|
|
1411
|
+
// Fetch file content (use raw content from the API)
|
|
1412
|
+
const { stdout: contentJson } = await exec(`gh api "repos/${owner}/${repo}/contents/${file.path}" --jq '.content'`);
|
|
1413
|
+
const content = Buffer.from(contentJson.trim().replace(/"/g, ''), 'base64').toString('utf-8');
|
|
1414
|
+
|
|
1415
|
+
const triggers = [];
|
|
1416
|
+
if (prTriggerPatterns.some(p => p.test(content))) {
|
|
1417
|
+
triggers.push('pull_request');
|
|
1418
|
+
}
|
|
1419
|
+
if (pushTriggerPatterns.some(p => p.test(content))) {
|
|
1420
|
+
triggers.push('push');
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (triggers.length > 0) {
|
|
1424
|
+
results.push({ name: file.name, triggers });
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (verbose) {
|
|
1428
|
+
console.log(`[VERBOSE] /merge: Workflow ${file.name}: triggers=[${triggers.join(', ')}]`);
|
|
1429
|
+
}
|
|
1430
|
+
} catch (fileError) {
|
|
1431
|
+
if (verbose) {
|
|
1432
|
+
console.log(`[VERBOSE] /merge: Error reading workflow file ${file.name}: ${fileError.message}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const hasPRTriggers = results.length > 0;
|
|
1438
|
+
|
|
1439
|
+
if (verbose) {
|
|
1440
|
+
console.log(`[VERBOSE] /merge: ${results.length}/${files.length} workflow files have PR/push triggers`);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return { hasPRTriggers, hasWorkflowFiles: true, workflows: results };
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
if (verbose) {
|
|
1446
|
+
console.log(`[VERBOSE] /merge: Error checking workflow PR triggers: ${error.message}`);
|
|
1447
|
+
}
|
|
1448
|
+
// On error, assume workflows might have PR triggers (safer: avoids false positives)
|
|
1449
|
+
return { hasPRTriggers: true, hasWorkflowFiles: true, workflows: [] };
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1289
1453
|
// Issue #1341: Re-export post-merge CI functions from separate module
|
|
1290
1454
|
import { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha } from './github-merge-ci.lib.mjs';
|
|
1291
1455
|
export { waitForCommitCI, checkBranchCIHealth, getMergeCommitSha };
|
|
@@ -1323,6 +1487,10 @@ export default {
|
|
|
1323
1487
|
checkBranchCIHealth,
|
|
1324
1488
|
getMergeCommitSha,
|
|
1325
1489
|
getActiveRepoWorkflows,
|
|
1490
|
+
// Issue #1480: Commit date, workflow PR triggers, and previous commit CI history for race condition detection
|
|
1491
|
+
getCommitDate,
|
|
1492
|
+
checkPreviousPRCommitsHadCI,
|
|
1493
|
+
checkWorkflowsHavePRTriggers,
|
|
1326
1494
|
// Issue #1413: Use issue timeline to find genuinely linked PRs (avoids false positives from text search)
|
|
1327
1495
|
getLinkedPRsFromTimeline,
|
|
1328
1496
|
};
|
package/src/github.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/hive.config.lib.mjs
CHANGED
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
|
-
//
|
|
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
|
-
//
|
|
474
|
-
|
|
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
|
-
//
|
|
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');
|
|
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
|
-
//
|
|
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
|
|
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) {
|
package/src/models/index.mjs
CHANGED
|
@@ -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'],
|
|
@@ -33,7 +33,7 @@ const { reportError } = sentryLib;
|
|
|
33
33
|
|
|
34
34
|
// Import GitHub merge functions
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
|
-
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows } = githubMergeLib;
|
|
36
|
+
const { checkPRMergeable, checkMergePermissions, mergePullRequest, waitForCI, checkForBillingLimitError, getRepoVisibility, BILLING_LIMIT_ERROR_PATTERN, getDetailedCIStatus, rerunWorkflowRun, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkPreviousPRCommitsHadCI, checkWorkflowsHavePRTriggers } = githubMergeLib;
|
|
37
37
|
|
|
38
38
|
// Import GitHub functions for log attachment
|
|
39
39
|
const githubLib = await import('./github.lib.mjs');
|
|
@@ -255,14 +255,86 @@ const getMergeBlockers = async (owner, repo, prNumber, verbose = false) => {
|
|
|
255
255
|
details: workflowRuns.map(r => r.name),
|
|
256
256
|
});
|
|
257
257
|
} else {
|
|
258
|
-
// No workflow runs for this SHA —
|
|
259
|
-
// Issue #
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
// No workflow runs for this SHA — but this could be a race condition!
|
|
259
|
+
// Issue #1480: GitHub Actions workflow runs take 30-120 seconds to appear in the
|
|
260
|
+
// API after a push. The previous fix (issue #1442) assumed 0 workflow runs meant
|
|
261
|
+
// "CI definitively NOT triggered", but this caused false positive "Ready to merge"
|
|
262
|
+
// when checked too soon after a push.
|
|
263
|
+
//
|
|
264
|
+
// Multi-layer defense (Issue #1480 enhanced):
|
|
265
|
+
// Layer 1: Grace period — check commit age
|
|
266
|
+
// Layer 2: Workflow file parsing — check .github/workflows for PR triggers
|
|
267
|
+
// Layer 3: Previous commit CI history — check if earlier PR commits had CI runs
|
|
268
|
+
const WORKFLOW_RUN_GRACE_PERIOD_SECONDS = 120; // 2 minutes — generous to cover slow GitHub API registration
|
|
269
|
+
const commitInfo = await getCommitDate(owner, repo, ciStatus.sha, verbose);
|
|
270
|
+
|
|
271
|
+
// Issue #1480: Parse workflow files for PR triggers (used in both grace period and post-grace checks)
|
|
272
|
+
const prTriggers = await checkWorkflowsHavePRTriggers(owner, repo, verbose);
|
|
273
|
+
|
|
274
|
+
// Issue #1480: If .github/workflows folder doesn't exist or has no workflow files,
|
|
275
|
+
// that's a definitive signal — no CI/CD will execute, skip grace period entirely
|
|
276
|
+
if (!prTriggers.hasWorkflowFiles) {
|
|
277
|
+
if (verbose) {
|
|
278
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} repo has no workflow files in .github/workflows/ — CI definitively not configured at file level`);
|
|
279
|
+
}
|
|
280
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (commitInfo.ageSeconds !== null && commitInfo.ageSeconds < WORKFLOW_RUN_GRACE_PERIOD_SECONDS) {
|
|
284
|
+
// Commit is recent — workflow runs may not have appeared in the API yet
|
|
285
|
+
if (verbose) {
|
|
286
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no workflow runs for SHA ${ciStatus.sha.substring(0, 7)}, but commit is only ${commitInfo.ageSeconds}s old (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s) — treating as potential race condition`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (prTriggers.hasPRTriggers) {
|
|
290
|
+
// Workflows have PR/push triggers AND commit is recent — almost certainly a race condition
|
|
291
|
+
if (verbose) {
|
|
292
|
+
console.log(`[VERBOSE] /merge: Workflow files confirm PR/push triggers exist (${prTriggers.workflows.map(w => w.name).join(', ')}) — waiting for workflow runs to appear`);
|
|
293
|
+
}
|
|
294
|
+
blockers.push({
|
|
295
|
+
type: 'ci_pending',
|
|
296
|
+
message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
|
|
297
|
+
details: prTriggers.workflows.map(w => w.name),
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
// No PR triggers found in workflow files — but commit is still recent, be safe and wait
|
|
301
|
+
if (verbose) {
|
|
302
|
+
console.log(`[VERBOSE] /merge: No PR/push triggers found in workflow files, but commit is only ${commitInfo.ageSeconds}s old — waiting to be safe`);
|
|
303
|
+
}
|
|
304
|
+
blockers.push({
|
|
305
|
+
type: 'ci_pending',
|
|
306
|
+
message: `CI/CD workflow runs have not appeared yet — commit is ${commitInfo.ageSeconds}s old, waiting for GitHub to register workflow runs (grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s)`,
|
|
307
|
+
details: [],
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// Commit is old enough (grace period elapsed) — but check additional signals before concluding
|
|
312
|
+
// Issue #1480: Layer 3 — Check if previous commits in this PR had CI runs.
|
|
313
|
+
// If earlier commits had CI, the HEAD commit should also have CI unless conditions changed.
|
|
314
|
+
const previousCI = await checkPreviousPRCommitsHadCI(owner, repo, prNumber, ciStatus.sha, verbose);
|
|
315
|
+
|
|
316
|
+
if (previousCI.hadPreviousCI && prTriggers.hasPRTriggers) {
|
|
317
|
+
// Previous commits had CI AND workflow files have PR triggers — something is wrong,
|
|
318
|
+
// this could be a GitHub API glitch or delayed registration beyond the grace period.
|
|
319
|
+
// Wait one more cycle to be safe.
|
|
320
|
+
if (verbose) {
|
|
321
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} previous commits had CI (${previousCI.previousCommitsWithCI}/${previousCI.totalPreviousCommits}) and workflows have PR triggers, but HEAD has no runs — waiting as safety measure`);
|
|
322
|
+
}
|
|
323
|
+
blockers.push({
|
|
324
|
+
type: 'ci_pending',
|
|
325
|
+
message: `CI/CD workflow runs missing for HEAD — previous PR commits had CI (${previousCI.previousCommitsWithCI} of ${previousCI.totalPreviousCommits}), workflows have PR triggers, possible API delay`,
|
|
326
|
+
details: prTriggers.workflows.map(w => w.name),
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
// CI was definitively NOT triggered
|
|
330
|
+
// Issue #1442: Fork PRs needing maintainer approval, paths-ignore filtering,
|
|
331
|
+
// workflow conditions not matching, etc. all result in zero workflow runs.
|
|
332
|
+
if (verbose) {
|
|
333
|
+
console.log(`[VERBOSE] /merge: PR #${prNumber} has no CI checks and no workflow runs for SHA ${ciStatus.sha.substring(0, 7)} (commit age: ${commitInfo.ageSeconds ?? 'unknown'}s, grace period: ${WORKFLOW_RUN_GRACE_PERIOD_SECONDS}s elapsed, previous CI: ${previousCI.hadPreviousCI}, PR triggers: ${prTriggers.hasPRTriggers}) — CI was not triggered`);
|
|
334
|
+
}
|
|
335
|
+
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
336
|
+
}
|
|
264
337
|
}
|
|
265
|
-
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: true };
|
|
266
338
|
}
|
|
267
339
|
} else {
|
|
268
340
|
// Repo has NO workflows — this is truly "no CI configured"
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -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))) {
|