@link-assistant/hive-mind 1.3.0 → 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 +19 -0
- package/package.json +1 -1
- package/src/git.lib.mjs +198 -0
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.validation.lib.mjs +75 -0
- package/src/solve.watch.lib.mjs +54 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
3
22
|
## 1.3.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/package.json
CHANGED
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
|
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -321,6 +321,11 @@ export const createYargsConfig = yargsInstance => {
|
|
|
321
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.',
|
|
322
322
|
default: true,
|
|
323
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
|
+
})
|
|
324
329
|
.parserConfiguration({
|
|
325
330
|
'boolean-negation': true,
|
|
326
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;
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -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...'));
|