@link-assistant/hive-mind 1.2.11 → 1.4.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,32 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 4a476ae: Add separate log comment for each auto-restart session with cost estimation
8
+ - Each auto-restart iteration now uploads its own session log with cost estimation to the PR
9
+ - Log comments use "Auto-restart X/Y Log" format instead of generic "Solution Draft Log"
10
+ - Issue #1107
11
+
12
+ ### Patch Changes
13
+
14
+ - 3239fa1: Add git identity validation to prevent commit failures
15
+ - Added `checkGitIdentity()` and `validateGitIdentity()` functions to validate git user configuration
16
+ - Added git identity check to `performSystemChecks()` that runs before any work begins
17
+ - Added `--auto-gh-configuration-repair` option that uses external `gh-setup-git-identity` command for automatic repair
18
+ - Added unit tests for identity validation
19
+
20
+ This fix prevents the "fatal: empty ident name" error that occurs when git user.name and user.email are not configured. When git identity is missing, users now see a clear error message with instructions for fixing it. The auto-repair feature requires the external [gh-setup-git-identity](https://github.com/link-foundation/gh-setup-git-identity) package to be installed.
21
+
22
+ ## 1.3.0
23
+
24
+ ### Minor Changes
25
+
26
+ - a403c0e: Add --auto-gitkeep-file option to automatically fallback to .gitkeep when CLAUDE.md is in .gitignore
27
+
28
+ This feature pre-checks if CLAUDE.md would be ignored by .gitignore BEFORE creating the file, preventing the "paths are ignored by one of your .gitignore files" error. When detected, automatically switches to .gitkeep mode. Enabled by default (--auto-gitkeep-file=true).
29
+
3
30
  ## 1.2.11
4
31
 
5
32
  ### Patch Changes
@@ -952,7 +979,7 @@
952
979
 
953
980
  This feature allows users to choose which file type to use for PR creation:
954
981
  - `--claude-file` (default: true): Use CLAUDE.md file for task details
955
- - `--gitkeep-file` (default: false, experimental): Use .gitkeep file instead
982
+ - `--gitkeep-file` (default: false): Use .gitkeep file instead
956
983
 
957
984
  The flags are mutually exclusive:
958
985
  - Using `--gitkeep-file` automatically disables `--claude-file`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.2.11",
3
+ "version": "1.4.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",
package/src/git.lib.mjs CHANGED
@@ -134,6 +134,201 @@ export const getGitVersionAsync = async ($, currentVersion) => {
134
134
  return currentVersion;
135
135
  };
136
136
 
137
+ /**
138
+ * Validates git user identity configuration
139
+ * Returns an object with validation status and identity info
140
+ *
141
+ * Git commits require both user.name and user.email to be set.
142
+ * This function checks both global (~/.gitconfig) and local (.git/config) configurations.
143
+ *
144
+ * See: https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup
145
+ * Related error: "fatal: empty ident name (for <>) not allowed"
146
+ *
147
+ * @param {function} execFunc - The exec function to use (for testing)
148
+ * @returns {Promise<{isValid: boolean, name: string|null, email: string|null, scope: string|null, error: string|null}>}
149
+ */
150
+ export const checkGitIdentity = async (execFunc = execAsync) => {
151
+ const result = {
152
+ isValid: false,
153
+ name: null,
154
+ email: null,
155
+ scope: null, // 'global', 'local', or 'none'
156
+ error: null,
157
+ };
158
+
159
+ try {
160
+ // Check for user.name
161
+ try {
162
+ const { stdout: nameStdout } = await execFunc('git config user.name', {
163
+ encoding: 'utf8',
164
+ env: process.env,
165
+ });
166
+ result.name = nameStdout.trim() || null;
167
+ } catch {
168
+ // user.name not set
169
+ result.name = null;
170
+ }
171
+
172
+ // Check for user.email
173
+ try {
174
+ const { stdout: emailStdout } = await execFunc('git config user.email', {
175
+ encoding: 'utf8',
176
+ env: process.env,
177
+ });
178
+ result.email = emailStdout.trim() || null;
179
+ } catch {
180
+ // user.email not set
181
+ result.email = null;
182
+ }
183
+
184
+ // Determine scope (check if local config exists)
185
+ if (result.name || result.email) {
186
+ try {
187
+ const { stdout: scopeStdout } = await execFunc('git config --show-origin user.name', {
188
+ encoding: 'utf8',
189
+ env: process.env,
190
+ });
191
+ // Output format: "file:/path/to/config\tvalue"
192
+ if (scopeStdout.includes('.git/config')) {
193
+ result.scope = 'local';
194
+ } else if (scopeStdout.includes('.gitconfig') || scopeStdout.includes('/etc/gitconfig')) {
195
+ result.scope = 'global';
196
+ } else {
197
+ result.scope = 'global';
198
+ }
199
+ } catch {
200
+ result.scope = 'none';
201
+ }
202
+ } else {
203
+ result.scope = 'none';
204
+ }
205
+
206
+ // Both name and email must be non-empty for valid git identity
207
+ // Empty string is also invalid (git rejects it)
208
+ result.isValid = !!(result.name && result.name.length > 0 && result.email && result.email.length > 0);
209
+
210
+ if (!result.isValid) {
211
+ const missing = [];
212
+ if (!result.name || result.name.length === 0) missing.push('user.name');
213
+ if (!result.email || result.email.length === 0) missing.push('user.email');
214
+ result.error = `Git identity incomplete: missing ${missing.join(' and ')}`;
215
+ }
216
+ } catch (error) {
217
+ result.error = `Failed to check git identity: ${error.message}`;
218
+ }
219
+
220
+ return result;
221
+ };
222
+
223
+ /**
224
+ * Validates git user identity and returns detailed error message if invalid
225
+ * Uses zx's $ for async execution
226
+ *
227
+ * @param {function} $ - The zx $ function
228
+ * @param {object} options - Options object
229
+ * @param {function} options.log - Log function for output
230
+ * @returns {Promise<boolean>} - True if identity is valid, false otherwise
231
+ */
232
+ export const validateGitIdentity = async ($, options = {}) => {
233
+ const { log = console.log } = options;
234
+
235
+ // Check user.name
236
+ let userName = null;
237
+ try {
238
+ const nameResult = await $`git config user.name 2>/dev/null || true`;
239
+ userName = nameResult.stdout.toString().trim() || null;
240
+ } catch {
241
+ userName = null;
242
+ }
243
+
244
+ // Check user.email
245
+ let userEmail = null;
246
+ try {
247
+ const emailResult = await $`git config user.email 2>/dev/null || true`;
248
+ userEmail = emailResult.stdout.toString().trim() || null;
249
+ } catch {
250
+ userEmail = null;
251
+ }
252
+
253
+ // Both must be set and non-empty
254
+ const isValid = !!(userName && userName.length > 0 && userEmail && userEmail.length > 0);
255
+
256
+ if (!isValid) {
257
+ const missing = [];
258
+ if (!userName || userName.length === 0) missing.push('user.name');
259
+ if (!userEmail || userEmail.length === 0) missing.push('user.email');
260
+
261
+ await log('');
262
+ await log('❌ Git identity not configured', { level: 'error' });
263
+ await log('');
264
+ await log(' Git commits require both user.name and user.email to be set.');
265
+ await log(` Missing: ${missing.join(' and ')}`);
266
+ await log('');
267
+ await log(' Current configuration:');
268
+ await log(` user.name: ${userName || '(not set)'}`);
269
+ await log(` user.email: ${userEmail || '(not set)'}`);
270
+ await log('');
271
+ await log(' 🔧 How to fix:');
272
+ await log('');
273
+ await log(' Option 1: Use GitHub CLI to set identity from your account');
274
+ await log(' gh-setup-git-identity');
275
+ await log('');
276
+ await log(' Option 2: Set identity manually');
277
+ await log(' git config --global user.name "Your Name"');
278
+ await log(' git config --global user.email "you@example.com"');
279
+ await log('');
280
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
281
+ await log('');
282
+ return false;
283
+ }
284
+
285
+ return true;
286
+ };
287
+
288
+ /**
289
+ * Attempts to repair git identity using gh-setup-git-identity --repair
290
+ * This function requires gh-setup-git-identity to be installed.
291
+ *
292
+ * @param {function} execFunc - The exec function to use (for testing)
293
+ * @returns {Promise<{success: boolean, error: string|null}>}
294
+ */
295
+ export const repairGitIdentity = async (execFunc = execAsync) => {
296
+ const result = {
297
+ success: false,
298
+ error: null,
299
+ };
300
+
301
+ try {
302
+ // First check if gh-setup-git-identity is installed
303
+ try {
304
+ await execFunc('which gh-setup-git-identity', {
305
+ encoding: 'utf8',
306
+ });
307
+ } catch {
308
+ result.error = 'gh-setup-git-identity is not installed. Please install it first or fix git identity manually.';
309
+ return result;
310
+ }
311
+
312
+ // Run gh-setup-git-identity --repair
313
+ await execFunc('gh-setup-git-identity --repair', {
314
+ encoding: 'utf8',
315
+ env: process.env,
316
+ });
317
+
318
+ // Check if the repair was successful by validating git identity
319
+ const identityCheck = await checkGitIdentity(execFunc);
320
+ if (identityCheck.isValid) {
321
+ result.success = true;
322
+ } else {
323
+ result.error = `Repair command completed but identity is still invalid: ${identityCheck.error}`;
324
+ }
325
+ } catch (error) {
326
+ result.error = `Failed to repair git identity: ${error.message}`;
327
+ }
328
+
329
+ return result;
330
+ };
331
+
137
332
  // Export all functions as default as well
138
333
  export default {
139
334
  isGitRepository,
@@ -142,4 +337,7 @@ export default {
142
337
  getCommitSha,
143
338
  getGitVersion,
144
339
  getGitVersionAsync,
340
+ checkGitIdentity,
341
+ validateGitIdentity,
342
+ repairGitIdentity,
145
343
  };
@@ -31,12 +31,20 @@ export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNum
31
31
 
32
32
  try {
33
33
  // Determine which file to create based on CLI flags
34
- const useClaudeFile = argv.claudeFile !== false; // Default to true
35
- const useGitkeepFile = argv.gitkeepFile === true; // Default to false
34
+ let useClaudeFile = argv.claudeFile !== false;
35
+ const useAutoGitkeepFile = argv.autoGitkeepFile !== false;
36
+
37
+ // Pre-check: If CLAUDE.md would be ignored by .gitignore, automatically switch to .gitkeep mode
38
+ if (useClaudeFile && useAutoGitkeepFile) {
39
+ const checkResult = await $({ cwd: tempDir, silent: true })`git check-ignore CLAUDE.md 2>/dev/null`;
40
+ if (checkResult.code === 0) {
41
+ await log(formatAligned('ℹ️', 'Pre-check:', 'CLAUDE.md is in .gitignore, switching to .gitkeep mode\n'));
42
+ useClaudeFile = false;
43
+ }
44
+ }
36
45
 
37
- // Log which mode we're using
38
46
  if (argv.verbose) {
39
- await log(` Using ${useClaudeFile ? 'CLAUDE.md' : '.gitkeep'} mode (--claude-file=${useClaudeFile}, --gitkeep-file=${useGitkeepFile})`, { verbose: true });
47
+ await log(` Using ${useClaudeFile ? 'CLAUDE.md' : '.gitkeep'} mode (--claude-file=${argv.claudeFile !== false}, --gitkeep-file=${argv.gitkeepFile === true}, --auto-gitkeep-file=${useAutoGitkeepFile})`, { verbose: true });
40
48
  }
41
49
 
42
50
  let filePath;
@@ -62,14 +70,13 @@ export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNum
62
70
  }
63
71
  }
64
72
  } else {
65
- // Create .gitkeep file directly (experimental mode)
66
- await log(formatAligned('📝', 'Creating:', '.gitkeep (experimental mode)'));
73
+ // .gitkeep mode (via explicit --gitkeep-file or auto-gitkeep-file fallback)
74
+ const modeDesc = argv.gitkeepFile === true ? '.gitkeep (explicit --gitkeep-file)' : '.gitkeep (CLAUDE.md is ignored)';
75
+ await log(formatAligned('📝', 'Creating:', modeDesc));
67
76
 
68
77
  filePath = path.join(tempDir, '.gitkeep');
69
78
  fileName = '.gitkeep';
70
-
71
- // .gitkeep files are typically small, no need to check for existing content
72
- // But we'll check if it exists for proper handling
79
+ // Check if .gitkeep already exists for proper handling
73
80
  try {
74
81
  existingContent = await fs.readFile(filePath, 'utf8');
75
82
  fileExisted = true;
@@ -125,12 +132,13 @@ Proceed.
125
132
  finalContent = taskInfo;
126
133
  }
127
134
  } else {
128
- // .gitkeep: Use minimal metadata format
135
+ // .gitkeep: Use minimal metadata format (explicit --gitkeep-file or auto-gitkeep-file fallback)
136
+ const creationReason = argv.gitkeepFile === true ? '# This file was created with --gitkeep-file flag' : '# This file was created because CLAUDE.md is in .gitignore (--auto-gitkeep-file=true)';
129
137
  const gitkeepContent = `# Auto-generated file for PR creation
130
138
  # Issue: ${issueUrl}
131
139
  # Branch: ${branchName}
132
140
  # Timestamp: ${timestamp}
133
- # This file was created with --gitkeep-file flag (experimental)
141
+ ${creationReason}
134
142
  # It will be removed when the task is complete`;
135
143
 
136
144
  if (fileExisted && existingContent) {
@@ -289,9 +297,8 @@ Proceed.
289
297
  }
290
298
 
291
299
  await log(formatAligned('📝', 'Creating commit:', `With ${commitFileName} file`));
292
-
293
- // Determine commit message based on which file is being committed
294
- const fileDesc = commitFileName === 'CLAUDE.md' ? 'CLAUDE.md with task information for AI processing' : `.gitkeep for PR creation (${useGitkeepFile ? 'created with --gitkeep-file flag (experimental)' : 'CLAUDE.md is in .gitignore'})`;
300
+ // Commit message distinguishes between explicit --gitkeep-file and auto-gitkeep-file fallback
301
+ const fileDesc = commitFileName === 'CLAUDE.md' ? 'CLAUDE.md with task information for AI processing' : `.gitkeep for PR creation (${argv.gitkeepFile === true ? 'created with --gitkeep-file flag' : 'CLAUDE.md is in .gitignore'})`;
295
302
  const commitMessage = `Initial commit with task details\n\nAdding ${fileDesc}.\nThis file will be removed when the task is complete.\n\nIssue: ${issueUrl}`;
296
303
 
297
304
  // Use explicit cwd option for better reliability
@@ -130,9 +130,14 @@ export const createYargsConfig = yargsInstance => {
130
130
  })
131
131
  .option('gitkeep-file', {
132
132
  type: 'boolean',
133
- description: 'Create .gitkeep file instead of CLAUDE.md (experimental, mutually exclusive with --claude-file)',
133
+ description: 'Create .gitkeep file instead of CLAUDE.md (mutually exclusive with --claude-file)',
134
134
  default: false,
135
135
  })
136
+ .option('auto-gitkeep-file', {
137
+ type: 'boolean',
138
+ description: 'Automatically use .gitkeep if CLAUDE.md is in .gitignore (pre-checks before creating file)',
139
+ default: true,
140
+ })
136
141
  .option('attach-logs', {
137
142
  type: 'boolean',
138
143
  description: 'Upload the solution draft log file to the Pull Request on completion (⚠️ WARNING: May expose sensitive data)',
@@ -316,6 +321,11 @@ export const createYargsConfig = yargsInstance => {
316
321
  description: 'Automatically remove .playwright-mcp/ folder before checking for uncommitted changes. This prevents browser automation artifacts from triggering auto-restart. Use --no-playwright-mcp-auto-cleanup to keep the folder for debugging.',
317
322
  default: true,
318
323
  })
324
+ .option('auto-gh-configuration-repair', {
325
+ type: 'boolean',
326
+ description: 'Automatically repair git configuration using gh-setup-git-identity --repair when git identity is not configured. Requires gh-setup-git-identity to be installed.',
327
+ default: false,
328
+ })
319
329
  .parserConfiguration({
320
330
  'boolean-negation': true,
321
331
  })
@@ -33,6 +33,10 @@ const {
33
33
  // isGitHubUrlType - not currently used
34
34
  } = githubLib;
35
35
 
36
+ // Import git-related functions for identity validation and repair
37
+ const gitLib = await import('./git.lib.mjs');
38
+ const { checkGitIdentity, repairGitIdentity } = gitLib;
39
+
36
40
  // Import Claude-related functions
37
41
  const claudeLib = await import('./claude.lib.mjs');
38
42
  // Import Sentry integration
@@ -217,6 +221,77 @@ export const performSystemChecks = async (minDiskSpace = 2048, skipToolConnectio
217
221
  return false;
218
222
  }
219
223
 
224
+ // Check git identity configuration before proceeding
225
+ // This prevents the "fatal: empty ident name" error during commits
226
+ // See: https://github.com/link-assistant/hive-mind/issues/1131
227
+ let gitIdentity = await checkGitIdentity();
228
+ if (!gitIdentity.isValid) {
229
+ // Check if auto-repair is enabled
230
+ if (argv.autoGhConfigurationRepair) {
231
+ await log('');
232
+ await log('⚠️ Git identity not configured, attempting auto-repair...', { level: 'warning' });
233
+ await log(` ${gitIdentity.error || 'Configuration is incomplete'}`);
234
+ await log('');
235
+
236
+ const repairResult = await repairGitIdentity();
237
+ if (repairResult.success) {
238
+ await log('✅ Git identity successfully repaired using gh-setup-git-identity --repair');
239
+ // Re-check identity to display the configured values
240
+ gitIdentity = await checkGitIdentity();
241
+ await log(` user.name: ${gitIdentity.name}`);
242
+ await log(` user.email: ${gitIdentity.email}`);
243
+ await log('');
244
+ } else {
245
+ await log('');
246
+ await log('❌ Auto-repair failed', { level: 'error' });
247
+ await log(` ${repairResult.error}`);
248
+ await log('');
249
+ await log(' Current configuration:');
250
+ await log(` user.name: ${gitIdentity.name || '(not set)'}`);
251
+ await log(` user.email: ${gitIdentity.email || '(not set)'}`);
252
+ await log('');
253
+ await log(' 🔧 How to fix manually:');
254
+ await log('');
255
+ await log(' Option 1: Install gh-setup-git-identity and use --auto-gh-configuration-repair');
256
+ await log(' npm install -g @link-foundation/gh-setup-git-identity');
257
+ await log('');
258
+ await log(' Option 2: Set identity manually');
259
+ await log(' git config --global user.name "Your Name"');
260
+ await log(' git config --global user.email "you@example.com"');
261
+ await log('');
262
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
263
+ await log('');
264
+ return false;
265
+ }
266
+ } else {
267
+ await log('');
268
+ await log('❌ Git identity not configured', { level: 'error' });
269
+ await log('');
270
+ await log(' Git commits require both user.name and user.email to be set.');
271
+ await log(` ${gitIdentity.error || 'Configuration is incomplete'}`);
272
+ await log('');
273
+ await log(' Current configuration:');
274
+ await log(` user.name: ${gitIdentity.name || '(not set)'}`);
275
+ await log(` user.email: ${gitIdentity.email || '(not set)'}`);
276
+ await log('');
277
+ await log(' 🔧 How to fix:');
278
+ await log('');
279
+ await log(' Option 1: Use GitHub CLI to set identity from your account');
280
+ await log(' gh-setup-git-identity');
281
+ await log('');
282
+ await log(' Option 2: Set identity manually');
283
+ await log(' git config --global user.name "Your Name"');
284
+ await log(' git config --global user.email "you@example.com"');
285
+ await log('');
286
+ await log(' Option 3: Enable auto-repair (requires gh-setup-git-identity)');
287
+ await log(' solve <issue-url> --auto-gh-configuration-repair');
288
+ await log('');
289
+ await log(' Related error: "fatal: empty ident name (for <>) not allowed"');
290
+ await log('');
291
+ return false;
292
+ }
293
+ }
294
+
220
295
  // Skip tool connection validation if in dry-run mode or explicitly requested
221
296
  if (!skipToolConnection) {
222
297
  let isToolConnected = false;
@@ -21,7 +21,7 @@ const fs = (await use('fs')).promises;
21
21
 
22
22
  // Import shared library functions
23
23
  const lib = await import('./lib.mjs');
24
- const { log, cleanErrorMessage, formatAligned } = lib;
24
+ const { log, cleanErrorMessage, formatAligned, getLogFile } = lib;
25
25
 
26
26
  // Import feedback detection functions
27
27
  const feedbackLib = await import('./solve.feedback.lib.mjs');
@@ -29,6 +29,10 @@ const feedbackLib = await import('./solve.feedback.lib.mjs');
29
29
  const sentryLib = await import('./sentry.lib.mjs');
30
30
  const { reportError } = sentryLib;
31
31
 
32
+ // Import GitHub functions for log attachment
33
+ const githubLib = await import('./github.lib.mjs');
34
+ const { sanitizeLogContent, attachLogToGitHub } = githubLib;
35
+
32
36
  const { detectAndCountFeedback } = feedbackLib;
33
37
 
34
38
  /**
@@ -517,6 +521,55 @@ export const watchForFeedback = async params => {
517
521
  }
518
522
  }
519
523
 
524
+ // Issue #1107: Attach log after each auto-restart session with its own cost estimation
525
+ // This ensures each restart has its own log comment instead of one combined log at the end
526
+ const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
527
+ if (isTemporaryWatch && prNumber && shouldAttachLogs) {
528
+ await log('');
529
+ await log(formatAligned('📎', 'Uploading auto-restart session log...', ''));
530
+ try {
531
+ const logFile = getLogFile();
532
+ if (logFile) {
533
+ // Use "Auto-restart X/Y Log" format as requested in issue #1107
534
+ const customTitle = `🔄 Auto-restart ${autoRestartCount}/${maxAutoRestartIterations} Log`;
535
+ const logUploadSuccess = await attachLogToGitHub({
536
+ logFile,
537
+ targetType: 'pr',
538
+ targetNumber: prNumber,
539
+ owner,
540
+ repo,
541
+ $,
542
+ log,
543
+ sanitizeLogContent,
544
+ verbose: argv.verbose,
545
+ customTitle,
546
+ sessionId: latestSessionId,
547
+ tempDir,
548
+ anthropicTotalCostUSD: latestAnthropicCost,
549
+ // Pass agent tool pricing data when available
550
+ publicPricingEstimate: toolResult.publicPricingEstimate,
551
+ pricingInfo: toolResult.pricingInfo,
552
+ });
553
+
554
+ if (logUploadSuccess) {
555
+ await log(formatAligned('', '✅ Auto-restart session log uploaded to PR', '', 2));
556
+ } else {
557
+ await log(formatAligned('', '⚠️ Could not upload auto-restart session log', '', 2));
558
+ }
559
+ }
560
+ } catch (logUploadError) {
561
+ reportError(logUploadError, {
562
+ context: 'attach_auto_restart_log',
563
+ prNumber,
564
+ owner,
565
+ repo,
566
+ autoRestartCount,
567
+ operation: 'upload_session_log',
568
+ });
569
+ await log(formatAligned('', `⚠️ Log upload error: ${cleanErrorMessage(logUploadError)}`, '', 2));
570
+ }
571
+ }
572
+
520
573
  await log('');
521
574
  if (isTemporaryWatch) {
522
575
  await log(formatAligned('✅', `${argv.tool.toUpperCase()} execution completed:`, 'Checking for remaining changes...'));