@link-assistant/hive-mind 1.32.3 → 1.34.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,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 614c3d9: Add model information display in PR/issue log comments. Shows actual models used (extracted from CLI JSON output) vs requested model. Main model is bolded when it matches the request; a warning appears when it doesn't. Supporting models are listed separately. Uses models.dev API for full model name, provider, and knowledge cutoff. Replaces duplicated tool name mapping with unified getToolDisplayName() helper.
8
+
9
+ ## 1.33.0
10
+
11
+ ### Minor Changes
12
+
13
+ - f7a2fdd: Add --auto-init-repository option to automatically initialize empty repositories by creating a simple README.md file, enabling branch creation and pull request workflows on repositories with no commits
14
+
3
15
  ## 1.32.3
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -341,12 +341,13 @@ solve <issue-url> [options]
341
341
 
342
342
  **Other useful options:**
343
343
 
344
- | Option | Alias | Description | Default |
345
- | --------------- | ----- | ------------------------------------------------ | ------- |
346
- | `--tool` | | AI tool (claude, opencode, codex, agent) | claude |
347
- | `--verbose` | `-v` | Enable verbose logging | false |
348
- | `--attach-logs` | | Attach logs to PR (⚠️ may expose sensitive data) | false |
349
- | `--help` | `-h` | Show all available options | - |
344
+ | Option | Alias | Description | Default |
345
+ | ------------------------ | ----- | ------------------------------------------------ | ------- |
346
+ | `--tool` | | AI tool (claude, opencode, codex, agent) | claude |
347
+ | `--verbose` | `-v` | Enable verbose logging | false |
348
+ | `--attach-logs` | | Attach logs to PR (⚠️ may expose sensitive data) | false |
349
+ | `--auto-init-repository` | | Auto-initialize empty repos (creates README.md) | false |
350
+ | `--help` | `-h` | Show all available options | - |
350
351
 
351
352
  > **📖 Full options list**: See [docs/CONFIGURATION.md](./docs/CONFIGURATION.md#solve-options) for all available options including forking, auto-continue, watch mode, and experimental features.
352
353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.32.3",
3
+ "version": "1.34.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",
@@ -10,26 +10,16 @@ import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTok
10
10
  export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeLogContent }; // Re-export for backward compatibility
11
11
  import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
12
12
  import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
13
+ // Import model info helpers (Issue #1225)
14
+ import { getToolDisplayName, getModelInfoForComment } from './model-info.lib.mjs';
15
+ // Re-export for use by other modules
16
+ export { getToolDisplayName };
13
17
 
14
- /**
15
- * Build cost estimation string for log comments
16
- * Issue #1250: Enhanced to show both public pricing estimate and actual provider cost
17
- *
18
- * @param {number|null} totalCostUSD - Public pricing estimate
19
- * @param {number|null} anthropicTotalCostUSD - Cost calculated by Anthropic (Claude-specific)
20
- * @param {Object|null} pricingInfo - Pricing info from agent tool
21
- * - opencodeCost: Actual billed cost from OpenCode Zen (for agent tool)
22
- * - isOpencodeFreeModel: Whether OpenCode Zen provides this model for free
23
- * - originalProvider: Original provider for pricing reference
24
- * - baseModelName: Base model name if pricing was derived from base model (Issue #1250)
25
- * @returns {string} Formatted cost info string for markdown (empty if no data available)
26
- */
18
+ /** Build cost estimation string for log comments (Issue #1250) */
27
19
  const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
28
- // Issue #1015: Don't show cost section when all values are unknown (clutters output)
29
20
  const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
30
21
  const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
31
22
  const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
32
- // Issue #1250: Check for OpenCode Zen actual cost
33
23
  const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
34
24
  if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
35
25
  let costInfo = '\n\n💰 **Cost estimation:**';
@@ -37,15 +27,10 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
37
27
  costInfo += `\n- Model: ${pricingInfo.modelName}`;
38
28
  if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
39
29
  }
40
- // Issue #1250: Show public pricing estimate based on original provider prices
41
30
  if (hasPublic) {
42
- // Issue #1250: For free models accessed via OpenCode Zen, show pricing based on base model
43
- // Only show as completely free if the base model also has no pricing
44
31
  if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
45
32
  costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
46
33
  } else {
47
- // Show actual public pricing estimate with original provider reference
48
- // Issue #1250: Include base model reference when pricing comes from base model
49
34
  let pricingRef = '';
50
35
  if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
51
36
  pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
@@ -57,7 +42,6 @@ const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) =
57
42
  } else if (hasPricing) {
58
43
  costInfo += '\n- Public pricing estimate: unknown';
59
44
  }
60
- // Issue #1250: Show actual cost from OpenCode Zen for agent tool
61
45
  if (hasOpencodeCost) {
62
46
  if (pricingInfo.isOpencodeFreeModel) {
63
47
  costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
@@ -360,26 +344,7 @@ Thank you! 🙏`;
360
344
  return false;
361
345
  }
362
346
  };
363
- /**
364
- * Attaches a log file to a GitHub PR or issue as a comment
365
- * @param {Object} options - Configuration options
366
- * @param {string} options.logFile - Path to the log file
367
- * @param {string} options.targetType - 'pr' or 'issue'
368
- * @param {number} options.targetNumber - PR or issue number
369
- * @param {string} options.owner - Repository owner
370
- * @param {string} options.repo - Repository name
371
- * @param {Function} options.$ - Command execution function
372
- * @param {Function} options.log - Logging function
373
- * @param {Function} options.sanitizeLogContent - Function to sanitize log content
374
- * @param {boolean} [options.verbose=false] - Enable verbose logging
375
- * @param {string} [options.errorMessage] - Error message to include in comment (for failure logs)
376
- * @param {string} [options.customTitle] - Custom title for the comment (defaults to "🤖 Solution Draft Log")
377
- * @param {boolean} [options.isUsageLimit] - Whether this is a usage limit error
378
- * @param {string} [options.limitResetTime] - Time when usage limit resets
379
- * @param {string} [options.toolName] - Name of the tool (claude, codex, opencode)
380
- * @param {string} [options.resumeCommand] - Command to resume the session
381
- * @returns {Promise<boolean>} - True if upload succeeded
382
- */
347
+ /** Attaches a log file to a GitHub PR or issue as a comment. Returns true if upload succeeded. */
383
348
  export async function attachLogToGitHub(options) {
384
349
  const fs = (await use('fs')).promises;
385
350
  const {
@@ -413,6 +378,9 @@ export async function attachLogToGitHub(options) {
413
378
  pricingInfo = null,
414
379
  // Issue #1088: Track error_during_execution for "Finished with errors" state
415
380
  errorDuringExecution = false,
381
+ // Issue #1225: Model information for PR comments
382
+ requestedModel = null, // The --model flag value (e.g., "opus", "sonnet")
383
+ tool = null, // The tool used (e.g., "claude", "agent", "codex", "opencode")
416
384
  } = options;
417
385
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
418
386
  const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
@@ -433,6 +401,8 @@ export async function attachLogToGitHub(options) {
433
401
  // Calculate token usage if sessionId and tempDir are provided
434
402
  // For agent tool, publicPricingEstimate is already provided, so we skip Claude-specific calculation
435
403
  let totalCostUSD = publicPricingEstimate;
404
+ // Issue #1225: Collect actual model IDs from Claude session JSON output
405
+ let actualModelIds = null;
436
406
  if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
437
407
  try {
438
408
  const { calculateSessionTokens } = await import('./claude.lib.mjs');
@@ -444,6 +414,13 @@ export async function attachLogToGitHub(options) {
444
414
  await log(` 💰 Calculated cost: $${totalCostUSD.toFixed(6)}`, { verbose: true });
445
415
  }
446
416
  }
417
+ // Extract actual model IDs from session data (Issue #1225)
418
+ if (tokenUsage.modelUsage && Object.keys(tokenUsage.modelUsage).length > 0) {
419
+ actualModelIds = Object.keys(tokenUsage.modelUsage);
420
+ if (verbose) {
421
+ await log(` 🤖 Actual models used: ${actualModelIds.join(', ')}`, { verbose: true });
422
+ }
423
+ }
447
424
  }
448
425
  } catch (tokenError) {
449
426
  // Don't fail the entire upload if token calculation fails
@@ -452,6 +429,25 @@ export async function attachLogToGitHub(options) {
452
429
  }
453
430
  }
454
431
  }
432
+ // For agent tool, extract actual model ID from pricingInfo (Issue #1225)
433
+ if (!actualModelIds && pricingInfo?.modelId) {
434
+ actualModelIds = [pricingInfo.modelId];
435
+ }
436
+ // Issue #1225: Fetch model information for comment using actual models from CLI output
437
+ let modelInfoString = '';
438
+ if (requestedModel || tool || actualModelIds) {
439
+ try {
440
+ modelInfoString = await getModelInfoForComment({ requestedModel, tool, pricingInfo, actualModelIds });
441
+ if (verbose && modelInfoString) {
442
+ await log(' 🤖 Model info fetched for comment', { verbose: true });
443
+ }
444
+ } catch (modelInfoError) {
445
+ // Non-critical: continue without model info
446
+ if (verbose) {
447
+ await log(` ⚠️ Could not fetch model info: ${modelInfoError.message}`, { verbose: true });
448
+ }
449
+ }
450
+ }
455
451
  // Read and sanitize log content
456
452
  const rawLogContent = await fs.readFile(logFile, 'utf8');
457
453
  if (verbose) {
@@ -523,7 +519,7 @@ ${resumeCommand}
523
519
  }
524
520
  }
525
521
 
526
- logComment += `
522
+ logComment += `${modelInfoString}
527
523
 
528
524
  <details>
529
525
  <summary>Click to expand execution log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -542,7 +538,7 @@ ${logContent}
542
538
  The automated solution draft encountered an error:
543
539
  \`\`\`
544
540
  ${errorMessage}
545
- \`\`\`
541
+ \`\`\`${modelInfoString}
546
542
 
547
543
  <details>
548
544
  <summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -559,7 +555,7 @@ ${logContent}
559
555
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
560
556
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
561
557
  logComment = `## ⚠️ Solution Draft Finished with Errors
562
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
558
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
563
559
 
564
560
  **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
565
561
 
@@ -592,7 +588,7 @@ ${logContent}
592
588
  sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
593
589
  }
594
590
  logComment = `## ${title}
595
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${sessionNote}
591
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}${sessionNote}
596
592
 
597
593
  <details>
598
594
  <summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
@@ -716,7 +712,7 @@ ${resumeCommand}
716
712
  }
717
713
  }
718
714
 
719
- logUploadComment += `
715
+ logUploadComment += `${modelInfoString}
720
716
 
721
717
  📎 **Execution log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
722
718
  🔗 [View complete execution log](${logUrl})
@@ -729,7 +725,7 @@ ${resumeCommand}
729
725
  The automated solution draft encountered an error:
730
726
  \`\`\`
731
727
  ${errorMessage}
732
- \`\`\`
728
+ \`\`\`${modelInfoString}
733
729
  📎 **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
734
730
  🔗 [View complete failure log](${logUrl})
735
731
  ---
@@ -738,7 +734,7 @@ ${errorMessage}
738
734
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
739
735
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
740
736
  logUploadComment = `## ⚠️ Solution Draft Finished with Errors
741
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
737
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
742
738
 
743
739
  **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
744
740
 
@@ -764,7 +760,7 @@ This log file contains the complete execution trace of the AI ${targetType === '
764
760
  sessionNote = '\n**Note**: This session was manually resumed using the --resume flag.\n';
765
761
  }
766
762
  logUploadComment = `## ${title}
767
- This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}
763
+ This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
768
764
  ${sessionNote}📎 **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
769
765
  🔗 [View complete solution draft log](${logUrl})
770
766
  ---
@@ -1482,6 +1478,7 @@ export default {
1482
1478
  checkGitHubPermissions,
1483
1479
  checkRepositoryWritePermission,
1484
1480
  attachLogToGitHub,
1481
+ getToolDisplayName,
1485
1482
  uploadLogWithGhUploadLog,
1486
1483
  fetchAllIssuesWithPagination,
1487
1484
  fetchProjectIssues,
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Model information library for hive-mind
5
+ * Provides unified model display, verification, and metadata fetching
6
+ * for all tools (Claude, Agent, OpenCode, Codex).
7
+ *
8
+ * @see https://github.com/link-assistant/hive-mind/issues/1225
9
+ */
10
+
11
+ // Check if use is already defined (when imported from solve.mjs)
12
+ // If not, fetch it (when running standalone)
13
+ if (typeof globalThis.use === 'undefined') {
14
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
15
+ }
16
+
17
+ import { log } from './lib.mjs';
18
+
19
+ /**
20
+ * Map tool identifier to user-friendly display name.
21
+ * Replaces duplicated ternary chains across the codebase.
22
+ * @param {string|null} tool - The tool identifier (claude, codex, opencode, agent)
23
+ * @returns {string} User-friendly display name
24
+ */
25
+ export const getToolDisplayName = tool => {
26
+ const name = (tool || '').toString().toLowerCase();
27
+ switch (name) {
28
+ case 'claude':
29
+ return 'Claude';
30
+ case 'codex':
31
+ return 'Codex';
32
+ case 'opencode':
33
+ return 'OpenCode';
34
+ case 'agent':
35
+ return 'Agent';
36
+ default:
37
+ return 'AI tool';
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Cached models.dev API response to avoid repeated network requests.
43
+ * The cache is per-process and cleared when the process exits.
44
+ */
45
+ let modelsDevCache = null;
46
+
47
+ /**
48
+ * Fetch the full models.dev API data with caching.
49
+ * @returns {Promise<Object|null>} The full API response or null on failure
50
+ */
51
+ const fetchModelsDevApi = async () => {
52
+ if (modelsDevCache) return modelsDevCache;
53
+ try {
54
+ const https = (await globalThis.use('https')).default;
55
+ return new Promise((resolve, reject) => {
56
+ https
57
+ .get('https://models.dev/api.json', res => {
58
+ let data = '';
59
+ res.on('data', chunk => {
60
+ data += chunk;
61
+ });
62
+ res.on('end', () => {
63
+ try {
64
+ modelsDevCache = JSON.parse(data);
65
+ resolve(modelsDevCache);
66
+ } catch (parseError) {
67
+ reject(parseError);
68
+ }
69
+ });
70
+ })
71
+ .on('error', err => {
72
+ reject(err);
73
+ });
74
+ });
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Fetch model metadata from models.dev API.
82
+ * Returns enriched model information including name, provider, version, and knowledge cutoff.
83
+ * @param {string} modelId - The model ID (e.g., "claude-opus-4-6", "opencode/grok-code")
84
+ * @returns {Promise<Object|null>} Model metadata or null if not found
85
+ */
86
+ export const fetchModelInfoForComment = async modelId => {
87
+ if (!modelId) return null;
88
+ try {
89
+ const apiData = await fetchModelsDevApi();
90
+ if (!apiData) return null;
91
+
92
+ // Normalize model ID: strip provider prefix for lookup (e.g., "anthropic/claude-3-5-sonnet" -> "claude-3-5-sonnet")
93
+ const lookupId = modelId.includes('/') ? modelId.split('/').pop() : modelId;
94
+
95
+ // Check Anthropic provider first (most common for Claude tools)
96
+ if (apiData.anthropic?.models?.[lookupId]) {
97
+ const modelInfo = { ...apiData.anthropic.models[lookupId] };
98
+ modelInfo.provider = apiData.anthropic.name || 'Anthropic';
99
+ return modelInfo;
100
+ }
101
+
102
+ // Search across all providers
103
+ for (const provider of Object.values(apiData)) {
104
+ if (provider.models && provider.models[lookupId]) {
105
+ const modelInfo = { ...provider.models[lookupId] };
106
+ modelInfo.provider = provider.name || provider.id;
107
+ return modelInfo;
108
+ }
109
+ }
110
+
111
+ // Try the full modelId (with provider prefix) as well
112
+ if (lookupId !== modelId) {
113
+ for (const provider of Object.values(apiData)) {
114
+ if (provider.models && provider.models[modelId]) {
115
+ const modelInfo = { ...provider.models[modelId] };
116
+ modelInfo.provider = provider.name || provider.id;
117
+ return modelInfo;
118
+ }
119
+ }
120
+ }
121
+
122
+ return null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ };
127
+
128
+ /**
129
+ * Normalize model ID for comparison purposes (strip suffixes, lowercase).
130
+ * @param {string} modelId - A model ID or alias
131
+ * @returns {string} Normalized ID
132
+ */
133
+ const normalizeForComparison = modelId => {
134
+ if (!modelId) return '';
135
+ return modelId
136
+ .toLowerCase()
137
+ .replace(/\[1m\]$/i, '')
138
+ .trim();
139
+ };
140
+
141
+ /**
142
+ * Check if a requested model alias matches an actual model ID.
143
+ * @param {string} requestedModel - The --model flag value (alias or full ID)
144
+ * @param {string} actualModelId - The actual model ID from CLI output
145
+ * @param {string|null} tool - The tool being used
146
+ * @returns {boolean}
147
+ */
148
+ const doesRequestedMatchActual = (requestedModel, actualModelId, tool) => {
149
+ if (!requestedModel || !actualModelId) return false;
150
+ const resolvedRequested = resolveModelId(requestedModel, tool);
151
+ const normResolved = normalizeForComparison(resolvedRequested);
152
+ const normActual = normalizeForComparison(actualModelId);
153
+ // Direct match
154
+ if (normResolved === normActual) return true;
155
+ // Partial match: resolved starts with actual or vice versa (for date-suffixed IDs)
156
+ if (normActual.startsWith(normResolved) || normResolved.startsWith(normActual)) return true;
157
+ return false;
158
+ };
159
+
160
+ /**
161
+ * Build model information string for PR/issue comments.
162
+ * Displays the requested model vs actual models used from CLI JSON output.
163
+ * The main model is bolded if it matches the requested model.
164
+ * A warning is shown if the main model doesn't match the requested model.
165
+ *
166
+ * @param {Object} options - Model info options
167
+ * @param {string|null} options.requestedModel - The model requested via --model flag (e.g., "opus")
168
+ * @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
169
+ * @param {Object|null} options.pricingInfo - Pricing info from tool result (agent tool provides modelId)
170
+ * @param {Object|null} options.modelInfo - Pre-fetched model metadata from models.dev (for first actual model)
171
+ * @param {Array<{modelId: string, modelInfo: Object|null}>|null} options.modelsUsed - Actual models used from CLI JSON output
172
+ * @returns {string} Formatted markdown string for model info section (empty if no data available)
173
+ */
174
+ export const buildModelInfoString = ({ requestedModel = null, tool = null, pricingInfo = null, modelInfo = null, modelsUsed = null } = {}) => {
175
+ const hasRequested = requestedModel !== null && requestedModel !== undefined;
176
+ const hasModelsUsed = Array.isArray(modelsUsed) && modelsUsed.length > 0;
177
+ const hasModelInfo = modelInfo !== null;
178
+ const hasPricingModel = pricingInfo?.modelId || pricingInfo?.modelName;
179
+
180
+ if (!hasRequested && !hasModelsUsed && !hasModelInfo && !hasPricingModel) return '';
181
+
182
+ let info = '\n\n🤖 **Models used:**';
183
+
184
+ // Display tool name
185
+ if (tool) {
186
+ info += `\n- Tool: ${getToolDisplayName(tool)}`;
187
+ }
188
+
189
+ // Display requested model (--model flag value)
190
+ if (hasRequested) {
191
+ info += `\n- Requested: \`${requestedModel}\``;
192
+ }
193
+
194
+ if (hasModelsUsed) {
195
+ // The first model is considered the "main" model
196
+ const [mainEntry, ...supportingEntries] = modelsUsed;
197
+ const mainModelId = mainEntry.modelId;
198
+ const mainModelMeta = mainEntry.modelInfo;
199
+
200
+ const mainMatches = hasRequested ? doesRequestedMatchActual(requestedModel, mainModelId, tool) : true;
201
+
202
+ // Build main model line
203
+ const mainModelName = mainModelMeta?.name || mainModelId;
204
+ const mainModelProvider = mainModelMeta?.provider || null;
205
+ const mainModelKnowledge = mainModelMeta?.knowledge || null;
206
+
207
+ if (mainMatches) {
208
+ info += `\n- **Main model: ${mainModelName}** (ID: \`${mainModelId}\`${mainModelProvider ? `, ${mainModelProvider}` : ''}${mainModelKnowledge ? `, cutoff: ${mainModelKnowledge}` : ''})`;
209
+ } else {
210
+ // Main model doesn't match requested - show warning
211
+ info += `\n- **Main model: ${mainModelName}** (ID: \`${mainModelId}\`${mainModelProvider ? `, ${mainModelProvider}` : ''}${mainModelKnowledge ? `, cutoff: ${mainModelKnowledge}` : ''})`;
212
+ if (hasRequested) {
213
+ info += `\n- ⚠️ **Warning**: Main model \`${mainModelId}\` does not match requested model \`${requestedModel}\``;
214
+ }
215
+ }
216
+
217
+ // Display supporting models
218
+ if (supportingEntries.length > 0) {
219
+ info += '\n- Supporting models:';
220
+ for (const entry of supportingEntries) {
221
+ const name = entry.modelInfo?.name || entry.modelId;
222
+ const provider = entry.modelInfo?.provider || null;
223
+ info += `\n - ${name} (\`${entry.modelId}\`${provider ? `, ${provider}` : ''})`;
224
+ }
225
+ }
226
+ } else if (hasModelInfo) {
227
+ // Fallback: single model info from models.dev (no actual CLI output data)
228
+ const mainModelName = modelInfo.name || (pricingInfo?.modelId ? pricingInfo.modelId : null) || 'Unknown';
229
+ info += `\n- Model: ${mainModelName}`;
230
+ if (modelInfo.id) info += ` (ID: \`${modelInfo.id}\`)`;
231
+ if (modelInfo.provider) info += `\n- Provider: ${modelInfo.provider}`;
232
+ if (modelInfo.knowledge) info += `\n- Knowledge cutoff: ${modelInfo.knowledge}`;
233
+ } else if (hasPricingModel) {
234
+ // Fallback to pricingInfo when no models.dev data
235
+ const modelId = pricingInfo.modelId || null;
236
+ const modelName = pricingInfo.modelName || modelId || 'Unknown';
237
+ if (modelId && modelId !== modelName) {
238
+ info += `\n- Model: ${modelName} (ID: \`${modelId}\`)`;
239
+ } else {
240
+ info += `\n- Model: ${modelName}`;
241
+ }
242
+ if (pricingInfo.provider) info += `\n- Provider: ${pricingInfo.provider}`;
243
+ }
244
+
245
+ return info;
246
+ };
247
+
248
+ /**
249
+ * Resolve the full model ID from a user-provided alias using the model mapping.
250
+ * @param {string|null} requestedModel - The model alias (e.g., "opus", "sonnet")
251
+ * @param {string|null} tool - The tool being used
252
+ * @returns {string|null} The full model ID or null
253
+ */
254
+ export const resolveModelId = (requestedModel, tool) => {
255
+ if (!requestedModel) return null;
256
+
257
+ try {
258
+ // Use model-mapping.lib.mjs mappings as authoritative source
259
+ const modelMaps = {
260
+ claude: {
261
+ sonnet: 'claude-sonnet-4-6',
262
+ opus: 'claude-opus-4-5-20251101',
263
+ haiku: 'claude-haiku-4-5-20251001',
264
+ 'opus-4-6': 'claude-opus-4-6',
265
+ 'opus-4-5': 'claude-opus-4-5-20251101',
266
+ 'sonnet-4-6': 'claude-sonnet-4-6',
267
+ 'sonnet-4-5': 'claude-sonnet-4-5-20250929',
268
+ 'haiku-4-5': 'claude-haiku-4-5-20251001',
269
+ },
270
+ agent: {
271
+ grok: 'opencode/grok-code',
272
+ 'grok-code': 'opencode/grok-code',
273
+ sonnet: 'anthropic/claude-3-5-sonnet',
274
+ opus: 'anthropic/claude-3-opus',
275
+ haiku: 'anthropic/claude-3-5-haiku',
276
+ },
277
+ opencode: {
278
+ gpt4: 'openai/gpt-4',
279
+ gpt4o: 'openai/gpt-4o',
280
+ sonnet: 'anthropic/claude-3-5-sonnet',
281
+ opus: 'anthropic/claude-3-opus',
282
+ grok: 'opencode/grok-code',
283
+ },
284
+ codex: {
285
+ gpt5: 'gpt-5',
286
+ 'gpt-5': 'gpt-5',
287
+ o3: 'o3',
288
+ gpt4: 'gpt-4',
289
+ gpt4o: 'gpt-4o',
290
+ sonnet: 'claude-3-5-sonnet',
291
+ opus: 'claude-3-opus',
292
+ },
293
+ };
294
+
295
+ const toolName = (tool || 'claude').toString().toLowerCase();
296
+ const map = modelMaps[toolName];
297
+ if (map) {
298
+ // Strip [1m] suffix if present (1M context window flag)
299
+ const cleanModel = requestedModel.replace(/\[1m\]$/i, '');
300
+ return map[cleanModel.toLowerCase()] || cleanModel;
301
+ }
302
+
303
+ return requestedModel;
304
+ } catch {
305
+ return requestedModel;
306
+ }
307
+ };
308
+
309
+ /**
310
+ * Fetch model info and build the complete model information string for PR comments.
311
+ * Uses actual models from CLI JSON output when available.
312
+ *
313
+ * @param {Object} options
314
+ * @param {string|null} options.requestedModel - The --model flag value
315
+ * @param {string|null} options.tool - The tool used (claude, agent, opencode, codex)
316
+ * @param {Object|null} options.pricingInfo - Pricing info from tool result
317
+ * @param {Array<string>|null} options.actualModelIds - Actual model IDs from CLI JSON output
318
+ * For Claude: from tokenUsage.modelUsage keys (model IDs used in session)
319
+ * For Agent: from pricingInfo.modelId
320
+ * @returns {Promise<string>} Formatted markdown model info section
321
+ */
322
+ export const getModelInfoForComment = async ({ requestedModel = null, tool = null, pricingInfo = null, actualModelIds = null } = {}) => {
323
+ // Determine the list of actual model IDs to display
324
+ // Priority: explicit actualModelIds > pricingInfo.modelId > resolve from requestedModel
325
+ let modelIds = [];
326
+
327
+ if (Array.isArray(actualModelIds) && actualModelIds.length > 0) {
328
+ modelIds = actualModelIds;
329
+ } else if (pricingInfo?.modelId) {
330
+ // Agent tool provides pricingInfo.modelId as the actual model used
331
+ modelIds = [pricingInfo.modelId];
332
+ } else if (requestedModel) {
333
+ // Fallback: resolve from requested model alias
334
+ const resolved = resolveModelId(requestedModel, tool);
335
+ if (resolved) modelIds = [resolved];
336
+ }
337
+
338
+ // Fetch model metadata from models.dev for each model ID
339
+ const modelsUsed = [];
340
+ for (const modelId of modelIds) {
341
+ let meta = null;
342
+ try {
343
+ meta = await fetchModelInfoForComment(modelId);
344
+ } catch {
345
+ await log(' ⚠️ Could not fetch model info from models.dev', { verbose: true });
346
+ }
347
+ modelsUsed.push({ modelId, modelInfo: meta });
348
+ }
349
+
350
+ // Determine which modelInfo to pass for legacy fallback (first model's metadata)
351
+ const firstModelInfo = modelsUsed.length > 0 ? modelsUsed[0].modelInfo : null;
352
+
353
+ return buildModelInfoString({
354
+ requestedModel,
355
+ tool,
356
+ pricingInfo,
357
+ modelInfo: modelsUsed.length === 0 ? firstModelInfo : null, // only used as fallback when no modelsUsed
358
+ modelsUsed: modelsUsed.length > 0 ? modelsUsed : null,
359
+ });
360
+ };
@@ -215,6 +215,7 @@ const KNOWN_OPTION_NAMES = [
215
215
  'prompt-examples-folder',
216
216
  'session-type',
217
217
  'working-directory',
218
+ 'auto-init-repository',
218
219
  'prompt-ensure-all-requirements-are-met',
219
220
  'finalize',
220
221
  'finalize-model',
@@ -790,6 +790,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
790
790
  anthropicTotalCostUSD: latestAnthropicCost,
791
791
  publicPricingEstimate: toolResult.publicPricingEstimate,
792
792
  pricingInfo: toolResult.pricingInfo,
793
+ // Issue #1225: Pass model and tool info for PR comments
794
+ requestedModel: argv.model,
795
+ tool: argv.tool || 'claude',
793
796
  });
794
797
  await log(formatAligned('', '✅ Session log uploaded to PR', '', 2));
795
798
  }
@@ -232,15 +232,35 @@ export async function handleBranchCreationError({ branchName, errorOutput, tempD
232
232
  await log(` ${line}`);
233
233
  }
234
234
  await log('');
235
- await log(' 💡 Possible causes:');
236
- await log(' • Branch name already exists');
237
- await log(' Uncommitted changes in repository');
238
- await log(' • Git configuration issues');
239
- await log('');
240
- await log(' 🔧 How to fix:');
241
- await log(' 1. Try running the command again (uses random names)');
242
- await log(` 2. Check git status: cd ${tempDir} && git status`);
243
- await log(` 3. View existing branches: cd ${tempDir} && git branch -a`);
235
+
236
+ // Check if this is an empty repository error (no commits to branch from)
237
+ const isEmptyRepoError = errorOutput.includes('is not a commit') || errorOutput.includes('not a valid object name') || errorOutput.includes('unknown revision');
238
+ if (isEmptyRepoError) {
239
+ await log(' 💡 Root cause:');
240
+ await log(' The repository appears to be empty (no commits).');
241
+ await log(' Cannot create a branch from a non-existent commit.');
242
+ await log('');
243
+ await log(' 🔧 How to fix:');
244
+ await log(' Use the --auto-init-repository flag to automatically initialize the repository:');
245
+ if (owner && repo) {
246
+ await log(` solve https://github.com/${owner}/${repo}/issues/<number> --auto-init-repository`);
247
+ } else {
248
+ await log(' solve <issue-url> --auto-init-repository');
249
+ }
250
+ await log('');
251
+ await log(' This will create a simple README.md file to make the repository non-empty,');
252
+ await log(' allowing branch creation and pull request workflows to proceed.');
253
+ } else {
254
+ await log(' 💡 Possible causes:');
255
+ await log(' • Branch name already exists');
256
+ await log(' • Uncommitted changes in repository');
257
+ await log(' • Git configuration issues');
258
+ await log('');
259
+ await log(' 🔧 How to fix:');
260
+ await log(' 1. Try running the command again (uses random names)');
261
+ await log(` 2. Check git status: cd ${tempDir} && git status`);
262
+ await log(` 3. View existing branches: cd ${tempDir} && git branch -a`);
263
+ }
244
264
  }
245
265
 
246
266
  export async function handleBranchVerificationError({ isContinueMode, branchName, actualBranch, prNumber, owner, repo, tempDir, formatAligned, log, $ }) {
@@ -362,6 +362,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
362
362
  description: 'Guide Claude to use agent-commander CLI (start-agent) instead of native Task tool for subagent delegation. Allows using any supported agent type (claude, opencode, codex, agent) with unified API. Only works with --tool claude and requires agent-commander to be installed.',
363
363
  default: false,
364
364
  },
365
+ 'auto-init-repository': {
366
+ type: 'boolean',
367
+ description: 'Automatically initialize empty repositories by creating a simple README.md file. Only works when you have write access to the repository. This allows branch creation and pull request workflows to proceed on repositories that have no commits.',
368
+ default: false,
369
+ },
365
370
  'attach-solution-summary': {
366
371
  type: 'boolean',
367
372
  description: 'Attach the AI solution summary (from the result field) as a comment to the PR/issue after completion. The summary is extracted from the AI tool JSON output and posted under a "Solution summary" header.',
@@ -54,6 +54,9 @@ export const handleFailure = async options => {
54
54
  sanitizeLogContent,
55
55
  verbose: argv.verbose,
56
56
  errorMessage: cleanErrorMessage(error),
57
+ // Issue #1225: Pass model and tool info for PR comments
58
+ requestedModel: argv.model,
59
+ tool: argv.tool || 'claude',
57
60
  });
58
61
  if (logUploadSuccess) {
59
62
  await log('📎 Failure log attached to Pull Request');
@@ -194,6 +194,9 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
194
194
  sanitizeLogContent,
195
195
  verbose: argv.verbose || false,
196
196
  errorMessage: cleanErrorMessage(error),
197
+ // Issue #1225: Pass model and tool info for PR comments
198
+ requestedModel: argv.model,
199
+ tool: argv.tool || 'claude',
197
200
  });
198
201
 
199
202
  if (logUploadSuccess) {
@@ -59,6 +59,9 @@ export const createInterruptWrapper = ({ cleanupContext, checkForUncommittedChan
59
59
  sanitizeLogContent,
60
60
  verbose: ctx.argv.verbose || false,
61
61
  errorMessage: 'Session interrupted by user (CTRL+C)',
62
+ // Issue #1225: Pass model and tool info for PR comments
63
+ requestedModel: ctx.argv.model,
64
+ tool: ctx.argv.tool || 'claude',
62
65
  });
63
66
  } catch (uploadError) {
64
67
  await log(`⚠️ Could not upload logs on interrupt: ${uploadError.message}`, {
package/src/solve.mjs CHANGED
@@ -50,7 +50,7 @@ const memoryCheck = await import('./memory-check.mjs');
50
50
  const lib = await import('./lib.mjs');
51
51
  const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo } = lib;
52
52
  const githubLib = await import('./github.lib.mjs');
53
- const { sanitizeLogContent, attachLogToGitHub } = githubLib;
53
+ const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
54
54
  const validation = await import('./solve.validation.lib.mjs');
55
55
  const { validateGitHubUrl, showAttachLogsWarning, initializeLogFile, validateUrlRequirement, validateContinueOnlyOnFeedback, performSystemChecks } = validation;
56
56
  const autoContinue = await import('./solve.auto-continue.lib.mjs');
@@ -79,26 +79,15 @@ const exitHandler = await import('./exit-handler.lib.mjs');
79
79
  const { initializeExitHandler, installGlobalExitHandlers, safeExit, logActiveHandles } = exitHandler;
80
80
  const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
81
81
  const getResourceSnapshot = memoryCheck.getResourceSnapshot;
82
-
83
- // Import new modular components
84
- const autoPrLib = await import('./solve.auto-pr.lib.mjs');
85
- const { handleAutoPrCreation } = autoPrLib;
86
- const repoSetupLib = await import('./solve.repo-setup.lib.mjs');
87
- const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = repoSetupLib;
88
- const branchLib = await import('./solve.branch.lib.mjs');
89
- const { createOrCheckoutBranch } = branchLib;
90
- const sessionLib = await import('./solve.session.lib.mjs');
91
- const { startWorkSession, endWorkSession, SESSION_TYPES } = sessionLib;
92
- const preparationLib = await import('./solve.preparation.lib.mjs');
93
- const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = preparationLib;
94
-
95
- // Import model validation library
96
- const modelValidation = await import('./model-validation.lib.mjs');
97
- const { validateAndExitOnInvalidModel } = modelValidation;
98
- const acceptInviteLib = await import('./solve.accept-invite.lib.mjs');
99
- const { autoAcceptInviteForRepo } = acceptInviteLib;
100
-
101
- // Initialize log file EARLY (use cwd initially, will be updated after argv parsing)
82
+ const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
83
+ const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
84
+ const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
85
+ const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
86
+ const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
87
+ const { validateAndExitOnInvalidModel } = await import('./model-validation.lib.mjs');
88
+ const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
89
+
90
+ // Initialize log file early (before argument parsing) to capture all output
102
91
  const logFile = await initializeLogFile(null);
103
92
 
104
93
  // Log version and raw command IMMEDIATELY after log file initialization (ensures they appear even if parsing fails)
@@ -534,11 +523,16 @@ try {
534
523
  });
535
524
 
536
525
  // Verify default branch and status using the new module
526
+ // Pass argv, owner, repo, issueUrl for empty repository auto-initialization (--auto-init-repository)
537
527
  const defaultBranch = await verifyDefaultBranchAndStatus({
538
528
  tempDir,
539
529
  log,
540
530
  formatAligned,
541
531
  $,
532
+ argv,
533
+ owner,
534
+ repo,
535
+ issueUrl,
542
536
  });
543
537
  // Create or checkout branch using the new module
544
538
  const branchName = await createOrCheckoutBranch({
@@ -939,9 +933,11 @@ try {
939
933
  // Mark this as a usage limit case for proper formatting
940
934
  isUsageLimit: true,
941
935
  limitResetTime: global.limitResetTime,
942
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
936
+ toolName: getToolDisplayName(argv.tool),
943
937
  resumeCommand,
944
938
  sessionId,
939
+ requestedModel: argv.model,
940
+ tool: argv.tool || 'claude',
945
941
  });
946
942
 
947
943
  if (logUploadSuccess) {
@@ -999,13 +995,15 @@ try {
999
995
  // Mark this as a usage limit case for proper formatting
1000
996
  isUsageLimit: true,
1001
997
  limitResetTime: global.limitResetTime,
1002
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
998
+ toolName: getToolDisplayName(argv.tool),
1003
999
  resumeCommand,
1004
1000
  sessionId,
1005
1001
  // Tell attachLogToGitHub that auto-resume is enabled to suppress CLI commands in the comment
1006
1002
  // See: https://github.com/link-assistant/hive-mind/issues/1152
1007
1003
  isAutoResumeEnabled: true,
1008
1004
  autoResumeMode: limitContinueMode,
1005
+ requestedModel: argv.model,
1006
+ tool: argv.tool || 'claude',
1009
1007
  });
1010
1008
 
1011
1009
  if (logUploadSuccess) {
@@ -1094,12 +1092,14 @@ try {
1094
1092
  // For usage limit, use a dedicated comment format to make it clear and actionable
1095
1093
  isUsageLimit: !!limitReached,
1096
1094
  limitResetTime: limitReached ? toolResult.limitResetTime : null,
1097
- toolName: (argv.tool || 'AI tool').toString().toLowerCase() === 'claude' ? 'Claude' : (argv.tool || 'AI tool').toString().toLowerCase() === 'codex' ? 'Codex' : (argv.tool || 'AI tool').toString().toLowerCase() === 'opencode' ? 'OpenCode' : (argv.tool || 'AI tool').toString().toLowerCase() === 'agent' ? 'Agent' : 'AI tool',
1095
+ toolName: getToolDisplayName(argv.tool),
1098
1096
  resumeCommand,
1099
1097
  // Include sessionId so the PR comment can present it
1100
1098
  sessionId,
1101
1099
  // If not a usage limit case, fall back to generic failure format
1102
1100
  errorMessage: limitReached ? undefined : `${argv.tool.toUpperCase()} execution failed`,
1101
+ requestedModel: argv.model,
1102
+ tool: argv.tool || 'claude',
1103
1103
  });
1104
1104
 
1105
1105
  if (logUploadSuccess) {
@@ -1351,6 +1351,8 @@ try {
1351
1351
  sessionId,
1352
1352
  tempDir,
1353
1353
  anthropicTotalCostUSD,
1354
+ requestedModel: argv.model,
1355
+ tool: argv.tool || 'claude',
1354
1356
  });
1355
1357
 
1356
1358
  if (logUploadSuccess) {
@@ -56,7 +56,7 @@ async function setupPrForkRemote(tempDir, argv, prForkOwner, repo, isContinueMod
56
56
  return await setupPrForkFn(tempDir, argv, prForkOwner, repo, isContinueMode, owner);
57
57
  }
58
58
 
59
- export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $ }) {
59
+ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned, $, argv, owner, repo, issueUrl }) {
60
60
  // Verify we're on the default branch and get its name
61
61
  const defaultBranchResult = await $({ cwd: tempDir })`git branch --show-current`;
62
62
 
@@ -66,27 +66,128 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
66
66
  throw new Error('Failed to get current branch');
67
67
  }
68
68
 
69
- const defaultBranch = defaultBranchResult.stdout.toString().trim();
69
+ let defaultBranch = defaultBranchResult.stdout.toString().trim();
70
70
  if (!defaultBranch) {
71
- await log('');
72
- await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
73
- await log('');
74
- await log(' 🔍 What happened:');
75
- await log(" Unable to determine the repository's default branch.");
76
- await log('');
77
- await log(' 💡 This might mean:');
78
- await log(' Repository is empty (no commits)');
79
- await log(' Unusual repository configuration');
80
- await log(' • Git command issues');
81
- await log('');
82
- await log(' 🔧 How to fix:');
83
- await log(' 1. Check repository status');
84
- await log(` 2. Verify locally: cd ${tempDir} && git branch`);
85
- await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
86
- await log('');
87
- throw new Error('Default branch detection failed');
71
+ // Repository is likely empty (no commits) - detect and handle
72
+ const isEmptyRepo = await detectEmptyRepository(tempDir, $);
73
+
74
+ if (isEmptyRepo && argv && argv.autoInitRepository && owner && repo) {
75
+ // --auto-init-repository is enabled, try to initialize
76
+ await log('');
77
+ await log(`${formatAligned('⚠️', 'EMPTY REPOSITORY', 'detected')}`, { level: 'warn' });
78
+ await log(`${formatAligned('', '', `Repository ${owner}/${repo} contains no commits`)}`);
79
+ await log(`${formatAligned('', '', '--auto-init-repository is enabled, attempting initialization...')}`);
80
+ await log('');
81
+
82
+ const repository = await import('./solve.repository.lib.mjs');
83
+ const { tryInitializeEmptyRepository } = repository;
84
+ const initialized = await tryInitializeEmptyRepository(owner, repo);
85
+
86
+ if (initialized) {
87
+ await log('');
88
+ await log(`${formatAligned('🔄', 'Re-fetching:', 'Pulling initialized repository...')}`);
89
+ // Wait for GitHub to process the new file
90
+ await new Promise(resolve => setTimeout(resolve, 2000));
91
+
92
+ // Re-fetch the origin to get the new commit
93
+ const fetchResult = await $({ cwd: tempDir })`git fetch origin`;
94
+ if (fetchResult.code !== 0) {
95
+ await log(`${formatAligned('❌', 'Fetch failed:', 'Could not fetch after initialization')}`, { level: 'error' });
96
+ throw new Error('Failed to fetch after empty repository initialization');
97
+ }
98
+
99
+ // Determine default branch name from the remote
100
+ const remoteHeadResult = await $({ cwd: tempDir })`git remote show origin`;
101
+ let remoteBranch = 'main'; // default fallback
102
+ if (remoteHeadResult.code === 0) {
103
+ const remoteOutput = remoteHeadResult.stdout.toString();
104
+ const headMatch = remoteOutput.match(/HEAD branch:\s*(\S+)/);
105
+ if (headMatch) {
106
+ remoteBranch = headMatch[1];
107
+ }
108
+ }
109
+
110
+ // Checkout the remote branch locally
111
+ const checkoutResult = await $({ cwd: tempDir })`git checkout -b ${remoteBranch} origin/${remoteBranch}`;
112
+ if (checkoutResult.code !== 0) {
113
+ // Try alternative: maybe the branch already exists locally somehow
114
+ const altResult = await $({ cwd: tempDir })`git checkout ${remoteBranch}`;
115
+ if (altResult.code !== 0) {
116
+ await log(`${formatAligned('❌', 'Checkout failed:', `Could not checkout ${remoteBranch} after initialization`)}`, { level: 'error' });
117
+ throw new Error('Failed to checkout branch after empty repository initialization');
118
+ }
119
+ }
120
+
121
+ defaultBranch = remoteBranch;
122
+ await log(`${formatAligned('✅', 'Repository initialized:', `Now on branch ${defaultBranch}`)}`);
123
+ await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
124
+ } else {
125
+ // Auto-init failed - provide helpful message with --auto-init-repository context
126
+ await log('');
127
+ await log(`${formatAligned('❌', 'AUTO-INIT FAILED', '')}`, { level: 'error' });
128
+ await log('');
129
+ await log(' 🔍 What happened:');
130
+ await log(` Repository ${owner}/${repo} is empty (no commits).`);
131
+ await log(' --auto-init-repository was enabled but initialization failed.');
132
+ await log(' You may not have write access to create files in the repository.');
133
+ await log('');
134
+ await log(' 💡 How to fix:');
135
+ await log(' Option 1: Ask repository owner to add initial content');
136
+ await log(' Even a simple README.md file would allow branch creation');
137
+ await log('');
138
+ await log(` Option 2: Manually initialize: gh api repos/${owner}/${repo}/contents/README.md \\`);
139
+ await log(' --method PUT --field message="Initialize repository" \\');
140
+ await log(' --field content="$(echo "# repo" | base64)"');
141
+ await log('');
142
+
143
+ // Post a comment on the issue informing about the empty repository
144
+ await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ });
145
+
146
+ throw new Error('Empty repository auto-initialization failed');
147
+ }
148
+ } else if (isEmptyRepo) {
149
+ // Empty repo detected but --auto-init-repository is not enabled
150
+ await log('');
151
+ await log(`${formatAligned('❌', 'EMPTY REPOSITORY DETECTED', '')}`, { level: 'error' });
152
+ await log('');
153
+ await log(' 🔍 What happened:');
154
+ await log(` The repository${owner && repo ? ` ${owner}/${repo}` : ''} is empty (no commits).`);
155
+ await log(' Cannot create branches or pull requests on an empty repository.');
156
+ await log('');
157
+ await log(' 💡 How to fix:');
158
+ await log(' Option 1: Use --auto-init-repository flag to automatically create a README.md');
159
+ await log(` solve <issue-url> --auto-init-repository`);
160
+ await log('');
161
+ await log(' Option 2: Ask repository owner to add initial content');
162
+ await log(' Even a simple README.md file would allow branch creation');
163
+ await log('');
164
+
165
+ // Post a comment on the issue informing about the empty repository
166
+ await tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ });
167
+
168
+ throw new Error('Empty repository detected - use --auto-init-repository to initialize');
169
+ } else {
170
+ // Not an empty repo, some other issue with branch detection
171
+ await log('');
172
+ await log(`${formatAligned('❌', 'DEFAULT BRANCH DETECTION FAILED', '')}`, { level: 'error' });
173
+ await log('');
174
+ await log(' 🔍 What happened:');
175
+ await log(" Unable to determine the repository's default branch.");
176
+ await log('');
177
+ await log(' 💡 This might mean:');
178
+ await log(' • Unusual repository configuration');
179
+ await log(' • Git command issues');
180
+ await log('');
181
+ await log(' 🔧 How to fix:');
182
+ await log(' 1. Check repository status');
183
+ await log(` 2. Verify locally: cd ${tempDir} && git branch`);
184
+ await log(` 3. Check remote: cd ${tempDir} && git branch -r`);
185
+ await log('');
186
+ throw new Error('Default branch detection failed');
187
+ }
188
+ } else {
189
+ await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
88
190
  }
89
- await log(`\n${formatAligned('📌', 'Default branch:', defaultBranch)}`);
90
191
 
91
192
  // Ensure we're on a clean default branch
92
193
  const statusResult = await $({ cwd: tempDir })`git status --porcelain`;
@@ -106,3 +207,79 @@ export async function verifyDefaultBranchAndStatus({ tempDir, log, formatAligned
106
207
 
107
208
  return defaultBranch;
108
209
  }
210
+
211
+ /**
212
+ * Try to post a comment on the issue informing the user about the empty repository.
213
+ * This is a non-critical operation - errors are silently ignored.
214
+ * When --auto-init-repository succeeds, no comment is posted (no action needed from the user).
215
+ */
216
+ async function tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, formatAligned, $ }) {
217
+ if (!issueUrl) return;
218
+
219
+ try {
220
+ const issueMatch = issueUrl.match(/\/issues\/(\d+)/);
221
+ if (!issueMatch) return;
222
+
223
+ const issueNumber = issueMatch[1];
224
+ await log(`${formatAligned('💬', 'Creating comment:', 'Informing about empty repository...')}`);
225
+
226
+ const commentBody = `## ⚠️ Repository Initialization Required
227
+
228
+ Hello! I attempted to work on this issue, but encountered a problem:
229
+
230
+ **Issue**: The repository is empty (no commits) and branches cannot be created.
231
+ **Reason**: Git cannot create branches in a repository with no commits.
232
+
233
+ ### 🔧 How to resolve:
234
+
235
+ **Option 1: Use \`--auto-init-repository\` flag**
236
+ Re-run the solver with the \`--auto-init-repository\` flag to automatically create a simple README.md:
237
+ \`\`\`
238
+ solve ${issueUrl} --auto-init-repository
239
+ \`\`\`
240
+
241
+ **Option 2: Initialize the repository yourself**
242
+ Please add initial content to the repository. Even a simple README.md (even if it is empty or contains just the title) file would make it possible to create branches and work on this issue.
243
+
244
+ Once the repository contains at least one commit with any file, I'll be able to proceed with solving this issue.
245
+
246
+ Thank you!`;
247
+
248
+ const commentResult = await $`gh issue comment ${issueNumber} --repo ${owner}/${repo} --body ${commentBody}`;
249
+ if (commentResult.code === 0) {
250
+ await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`);
251
+ } else {
252
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
253
+ }
254
+ } catch {
255
+ // Silently ignore comment creation errors - not critical to the process
256
+ await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Detect if a cloned repository is empty (has no commits).
262
+ * An empty repository has no branches and no commits.
263
+ */
264
+ async function detectEmptyRepository(tempDir, $) {
265
+ // Check if there are any commits in the repository
266
+ const logResult = await $({ cwd: tempDir })`git rev-parse HEAD 2>&1`;
267
+ if (logResult.code !== 0) {
268
+ // git rev-parse HEAD fails when there are no commits
269
+ const output = (logResult.stdout || logResult.stderr || '').toString();
270
+ if (output.includes('unknown revision') || output.includes('bad default revision') || output.includes('does not have any commits')) {
271
+ return true;
272
+ }
273
+ }
274
+
275
+ // Also check if there are any remote branches
276
+ const remoteBranchResult = await $({ cwd: tempDir })`git branch -r`;
277
+ if (remoteBranchResult.code === 0) {
278
+ const branches = remoteBranchResult.stdout.toString().trim();
279
+ if (!branches) {
280
+ return true;
281
+ }
282
+ }
283
+
284
+ return false;
285
+ }
@@ -317,8 +317,9 @@ export const setupTempDirectory = async (argv, workspaceInfo = null) => {
317
317
  };
318
318
 
319
319
  // Try to initialize an empty repository by creating a simple README.md
320
- // This makes the repository forkable
321
- const tryInitializeEmptyRepository = async (owner, repo) => {
320
+ // This makes the repository forkable and allows branch creation
321
+ // Exported for use in solve.repo-setup.lib.mjs (direct access path for empty repos)
322
+ export const tryInitializeEmptyRepository = async (owner, repo) => {
322
323
  try {
323
324
  await log(`${formatAligned('🔧', 'Auto-fix:', 'Attempting to initialize empty repository...')}`);
324
325
 
@@ -675,6 +675,9 @@ Fixes ${issueRef}
675
675
  errorDuringExecution,
676
676
  // Issue #1152: Pass sessionType for differentiated log comments
677
677
  sessionType,
678
+ // Issue #1225: Pass model and tool info for PR comments
679
+ requestedModel: argv.model,
680
+ tool: argv.tool || 'claude',
678
681
  });
679
682
  }
680
683
 
@@ -754,6 +757,9 @@ Fixes ${issueRef}
754
757
  errorDuringExecution,
755
758
  // Issue #1152: Pass sessionType for differentiated log comments
756
759
  sessionType,
760
+ // Issue #1225: Pass model and tool info for issue comments
761
+ requestedModel: argv.model,
762
+ tool: argv.tool || 'claude',
757
763
  });
758
764
  }
759
765
 
@@ -838,6 +844,9 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
838
844
  sanitizeLogContent,
839
845
  verbose: argv.verbose || false,
840
846
  errorMessage: cleanErrorMessage(error),
847
+ // Issue #1225: Pass model and tool info for PR comments
848
+ requestedModel: argv.model,
849
+ tool: argv.tool || 'claude',
841
850
  });
842
851
 
843
852
  if (logUploadSuccess) {
@@ -297,6 +297,9 @@ export const watchForFeedback = async params => {
297
297
  // Mark if this was a usage limit failure
298
298
  isUsageLimit: toolResult.limitReached,
299
299
  limitResetTime: toolResult.limitResetTime,
300
+ // Issue #1225: Pass model and tool info for PR comments
301
+ requestedModel: argv.model,
302
+ tool: argv.tool || 'claude',
300
303
  });
301
304
 
302
305
  if (logUploadSuccess) {
@@ -363,6 +366,9 @@ export const watchForFeedback = async params => {
363
366
  // Pass agent tool pricing data when available
364
367
  publicPricingEstimate: toolResult.publicPricingEstimate,
365
368
  pricingInfo: toolResult.pricingInfo,
369
+ // Issue #1225: Pass model and tool info for PR comments
370
+ requestedModel: argv.model,
371
+ tool: argv.tool || 'claude',
366
372
  });
367
373
 
368
374
  if (logUploadSuccess) {