@link-assistant/hive-mind 1.72.5 → 1.72.7

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.72.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 61e2935: Fix "Failed to add .gitkeep" abort during auto-PR creation when the target repository's `.gitignore` matches the seed placeholder (issue #1825). Placeholder staging now routes through `addPlaceholderFileToGit`, which detects the ignored path with `git check-ignore` and retries with `git add -f`. Because the placeholder is created by the solver to seed the initial commit and removed once the task completes, force-adding it is safe.
8
+
9
+ ## 1.72.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 57f15ec: Detect same-account human feedback in auto-restart comment monitoring only when the AI tool is idle, while still filtering hive-mind tool-generated comments by marker and tracked ID.
14
+
3
15
  ## 1.72.5
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.72.5",
3
+ "version": "1.72.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -76,7 +76,7 @@ const formatRunLine = run => {
76
76
  // search scope for checkForExistingComment() stays in lock-step with the
77
77
  // markers actually embedded in tool-posted comments.
78
78
  const toolComments = await import('./tool-comments.lib.mjs');
79
- const { SESSION_ENDING_MARKERS } = toolComments;
79
+ const { SESSION_ENDING_MARKERS, isToolGeneratedComment, isToolTrackedCommentId } = toolComments;
80
80
 
81
81
  /**
82
82
  * Issue #1323: Check if a comment with specific content already exists on the PR
@@ -168,14 +168,25 @@ export const checkForExistingComment = async (owner, repo, prNumber, commentSign
168
168
 
169
169
  /**
170
170
  * Check for new comments from non-bot users since last commit
171
+ *
172
+ * Same-account comments are only considered feedback when
173
+ * `trustAuthenticatedUserComments` is true. Keep the default false for callers
174
+ * that may run while an AI tool is still active: those tools can post through
175
+ * the authenticated GitHub account.
176
+ *
177
+ * @param {Function} commandRunner - Tagged-template command runner, injectable for tests
178
+ * @param {Object} options - Comment classification options
179
+ * @param {boolean} options.trustAuthenticatedUserComments - True only when the caller knows the AI tool is not running
171
180
  * @returns {Promise<{hasNewComments: boolean, comments: Array}>}
172
181
  */
173
- export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCheckTime, verbose = false) => {
182
+ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber, lastCheckTime, verbose = false, commandRunner = $, options = {}) => {
174
183
  try {
184
+ const { trustAuthenticatedUserComments = false } = options;
185
+
175
186
  // Get current GitHub user to identify which comments are from the bot/hive-mind
176
187
  let currentUser = null;
177
188
  try {
178
- const userResult = await $`gh api user --jq .login`;
189
+ const userResult = await commandRunner`gh api user --jq .login`;
179
190
  if (userResult.code === 0) {
180
191
  currentUser = userResult.stdout.toString().trim();
181
192
  }
@@ -183,7 +194,12 @@ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber,
183
194
  // If we can't get the current user, continue without filtering
184
195
  }
185
196
 
186
- // Common bot usernames and patterns to filter out
197
+ // Common bot usernames and patterns to filter out.
198
+ // Issue #1821: In same-account operation, humans and AI tools can both
199
+ // post through the authenticated account. The safe default treats that
200
+ // account as tool-owned; auto-restart-until-mergeable opts in to trusting
201
+ // same-account comments only while no AI tool execution is active, and
202
+ // still filters tool-generated comments by tracked IDs and marker strings.
187
203
  // Note: Patterns use word boundaries or end-of-string to avoid false positives
188
204
  // (e.g., "claudeuser" should NOT match as a bot)
189
205
  const botPatterns = [
@@ -201,21 +217,21 @@ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber,
201
217
 
202
218
  const isBot = login => {
203
219
  if (!login) return false;
204
- // Check if it's the current user (the bot running hive-mind)
205
- if (currentUser && login === currentUser) return true;
206
220
  // Check against known bot patterns
207
221
  return botPatterns.some(pattern => pattern.test(login));
208
222
  };
209
223
 
224
+ const isToolComment = comment => isToolTrackedCommentId(comment.id) || isToolGeneratedComment(comment.body);
225
+
210
226
  // Fetch PR conversation comments
211
- const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
227
+ const prCommentsResult = await commandRunner`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
212
228
  let prComments = [];
213
229
  if (prCommentsResult.code === 0 && prCommentsResult.stdout) {
214
230
  prComments = JSON.parse(prCommentsResult.stdout.toString() || '[]');
215
231
  }
216
232
 
217
233
  // Fetch PR review comments (inline code comments)
218
- const prReviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
234
+ const prReviewCommentsResult = await commandRunner`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
219
235
  let prReviewComments = [];
220
236
  if (prReviewCommentsResult.code === 0 && prReviewCommentsResult.stdout) {
221
237
  prReviewComments = JSON.parse(prReviewCommentsResult.stdout.toString() || '[]');
@@ -224,7 +240,7 @@ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber,
224
240
  // Fetch issue comments if we have an issue number
225
241
  let issueComments = [];
226
242
  if (issueNumber && issueNumber !== prNumber) {
227
- const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
243
+ const issueCommentsResult = await commandRunner`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
228
244
  if (issueCommentsResult.code === 0 && issueCommentsResult.stdout) {
229
245
  issueComments = JSON.parse(issueCommentsResult.stdout.toString() || '[]');
230
246
  }
@@ -233,14 +249,28 @@ export const checkForNonBotComments = async (owner, repo, prNumber, issueNumber,
233
249
  // Combine all comments
234
250
  const allComments = [...prComments, ...prReviewComments, ...issueComments];
235
251
 
236
- // Filter for new comments from non-bot users
252
+ // Filter for new comments from non-bot users. Automated hive-mind/tool
253
+ // comments are excluded by marker/ID, including comments posted by the
254
+ // authenticated user during the current or a previous process.
237
255
  const newNonBotComments = allComments.filter(comment => {
238
256
  const commentTime = new Date(comment.created_at);
239
257
  const isAfterLastCheck = commentTime > lastCheckTime;
240
- const isFromNonBot = !isBot(comment.user?.login);
241
-
242
- if (verbose && isAfterLastCheck && isFromNonBot) {
243
- console.log(`[VERBOSE] New non-bot comment from ${comment.user?.login} at ${comment.created_at}`);
258
+ const login = comment.user?.login;
259
+ const isFromAuthenticatedUser = Boolean(currentUser && login === currentUser);
260
+ const isFromTool = isToolComment(comment);
261
+ const isFromAuthenticatedUserToolContext = isFromAuthenticatedUser && !trustAuthenticatedUserComments;
262
+ const isFromBot = isBot(login) || isFromAuthenticatedUserToolContext;
263
+ const isFromNonBot = !isFromBot && !isFromTool;
264
+
265
+ if (verbose && isAfterLastCheck && isFromTool) {
266
+ console.log(`[VERBOSE] Skipping tool-generated comment from ${login} at ${comment.created_at}`);
267
+ } else if (verbose && isAfterLastCheck && isFromAuthenticatedUserToolContext) {
268
+ console.log(`[VERBOSE] Skipping authenticated-user comment from ${login} at ${comment.created_at} because same-account feedback is not trusted in this context`);
269
+ } else if (verbose && isAfterLastCheck && isFromBot) {
270
+ console.log(`[VERBOSE] Skipping bot comment from ${login} at ${comment.created_at}`);
271
+ } else if (verbose && isAfterLastCheck && isFromNonBot) {
272
+ const sameAccountSuffix = currentUser && login === currentUser ? ' (authenticated user)' : '';
273
+ console.log(`[VERBOSE] New non-bot comment from ${login}${sameAccountSuffix} at ${comment.created_at}`);
244
274
  }
245
275
 
246
276
  return isAfterLastCheck && isFromNonBot;
@@ -206,8 +206,12 @@ export const watchUntilMergeable = async params => {
206
206
  // Keep the counter as-is (it reached the safety valve or wasn't needed).
207
207
  }
208
208
 
209
- // Check for new comments from non-bot users
210
- const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose);
209
+ // Check for new comments from non-bot users. At this point the AI tool
210
+ // is not executing, so same-account non-tool comments can be trusted as
211
+ // human feedback while known tool comments remain filtered by markers/IDs.
212
+ const { hasNewComments, comments } = await checkForNonBotComments(owner, repo, prNumber, issueNumber, lastCheckTime, argv.verbose, $, {
213
+ trustAuthenticatedUserComments: true,
214
+ });
211
215
 
212
216
  // Check for uncommitted changes using shared utility
213
217
  const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Placeholder-staging helper for auto PR creation.
3
+ *
4
+ * Extracted from solve.auto-pr.lib.mjs (issue #1825) to keep that module under
5
+ * the max-lines lint budget.
6
+ */
7
+
8
+ /**
9
+ * Stage the temporary placeholder file (CLAUDE.md or .gitkeep) used to seed the
10
+ * initial auto-PR commit.
11
+ *
12
+ * The solver writes this placeholder deliberately to create the first commit so
13
+ * a draft PR can be opened, and removes it again once the task completes (see
14
+ * cleanupClaudeFile in solve.results.lib.mjs). When the target repository's
15
+ * .gitignore matches the placeholder — issue #1825: e.g. rumaster/tg-games
16
+ * ignores `.gitkeep` — a plain `git add <file>` exits non-zero with
17
+ * "The following paths are ignored by one of your .gitignore files", which
18
+ * previously aborted PR creation with a fatal "Failed to add .gitkeep".
19
+ *
20
+ * Because the placeholder belongs to us and is short-lived, we confirm the path
21
+ * is actually ignored with `git check-ignore` and then retry with
22
+ * `git add -f`. Force-adding only happens for the ignored-placeholder case;
23
+ * any other add failure is surfaced unchanged so genuine errors are not masked.
24
+ *
25
+ * @param {object} params
26
+ * @param {Function} params.$ - command-stream tagged-template runner.
27
+ * @param {string} params.tempDir - repository working directory.
28
+ * @param {string} params.fileName - placeholder file name (CLAUDE.md or .gitkeep).
29
+ * @param {Function} [params.log] - async logger.
30
+ * @param {Function} [params.formatAligned] - log line formatter.
31
+ * @param {boolean} [params.verbose] - emit verbose diagnostics.
32
+ * @returns {Promise<{code: number, forced: boolean, ignored: boolean, stderr: string}>}
33
+ */
34
+ export async function addPlaceholderFileToGit({ $, tempDir, fileName, log, formatAligned, verbose = false }) {
35
+ // Run silently: `git add` is quiet on success and only emits the noisy
36
+ // "paths are ignored ... Use -f" hint on failure, which we capture in
37
+ // `stderr` and re-surface from the caller only when the failure is genuine.
38
+ const addResult = await $({ cwd: tempDir, silent: true })`git add ${fileName}`;
39
+ if (addResult.code === 0) {
40
+ return { code: 0, forced: false, ignored: false, stderr: '' };
41
+ }
42
+
43
+ const stderr = addResult.stderr ? addResult.stderr.toString() : '';
44
+
45
+ // Determine whether the add failed because the placeholder is git-ignored.
46
+ // `git check-ignore` exits 0 when the path matches a .gitignore rule.
47
+ const checkIgnore = await $({ cwd: tempDir, silent: true })`git check-ignore ${fileName}`;
48
+ const ignored = checkIgnore.code === 0;
49
+
50
+ if (!ignored) {
51
+ // The failure was not caused by .gitignore — surface the original error so
52
+ // genuine problems (permissions, corrupt index, ...) are not masked.
53
+ return { code: addResult.code, forced: false, ignored: false, stderr };
54
+ }
55
+
56
+ if (log && formatAligned) {
57
+ await log(formatAligned('ℹ️', `${fileName} is ignored:`, 'Force-adding placeholder (git add -f)'));
58
+ }
59
+ if (verbose && log) {
60
+ await log(` ${fileName} matched a .gitignore rule; retrying with: git add -f ${fileName}`, { verbose: true });
61
+ }
62
+
63
+ const forcedResult = await $({ cwd: tempDir, silent: true })`git add -f ${fileName}`;
64
+ return {
65
+ code: forcedResult.code,
66
+ forced: true,
67
+ ignored: true,
68
+ stderr: forcedResult.stderr ? forcedResult.stderr.toString() : '',
69
+ };
70
+ }
@@ -8,6 +8,7 @@ import { handleRejectedPushForAutoPr, synchronizeExistingIssueBranchBeforeAutoPr
8
8
  import { emitForkAwareDiagnostic } from './solve.auto-pr-fork-diagnostic.lib.mjs';
9
9
 
10
10
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. Issue #1756: execGhWithRetry retries on transient 5xx (504) too.
11
+ import { addPlaceholderFileToGit } from './solve.auto-pr-placeholder.lib.mjs'; // Issue #1825: force-adds the seed placeholder when the target repo gitignores it.
11
12
 
12
13
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
13
14
  // Skip auto-PR creation if:
@@ -166,12 +167,13 @@ Proceed.
166
167
  // Add and commit the file
167
168
  await log(formatAligned('📦', 'Adding file:', 'To git staging'));
168
169
 
169
- // Use explicit cwd option for better reliability
170
- const addResult = await $({ cwd: tempDir })`git add ${fileName}`;
170
+ // Issue #1825: force-adds the placeholder when the target repo gitignores
171
+ // it (e.g. ignores `.gitkeep`), so PR creation is no longer aborted.
172
+ const addResult = await addPlaceholderFileToGit({ $, tempDir, fileName, log, formatAligned, verbose: argv.verbose });
171
173
 
172
174
  if (addResult.code !== 0) {
173
175
  await log(`❌ Failed to add ${fileName}`, { level: 'error' });
174
- await log(` Error: ${addResult.stderr ? addResult.stderr.toString() : 'Unknown error'}`, { level: 'error' });
176
+ await log(` Error: ${addResult.stderr || 'Unknown error'}`, { level: 'error' });
175
177
  throw new Error(`Failed to add ${fileName}`);
176
178
  }
177
179
 
@@ -212,12 +214,12 @@ Proceed.
212
214
  await fs.writeFile(gitkeepPath, gitkeepContent);
213
215
  await log(formatAligned('✅', 'Created:', '.gitkeep file'));
214
216
 
215
- // Try to add .gitkeep
216
- const gitkeepAddResult = await $({ cwd: tempDir })`git add .gitkeep`;
217
+ // Try to add .gitkeep (force-added if it too is gitignored — issue #1825)
218
+ const gitkeepAddResult = await addPlaceholderFileToGit({ $, tempDir, fileName: '.gitkeep', log, formatAligned, verbose: argv.verbose });
217
219
 
218
220
  if (gitkeepAddResult.code !== 0) {
219
221
  await log('❌ Failed to add .gitkeep', { level: 'error' });
220
- await log(` Error: ${gitkeepAddResult.stderr ? gitkeepAddResult.stderr.toString() : 'Unknown error'}`, {
222
+ await log(` Error: ${gitkeepAddResult.stderr || 'Unknown error'}`, {
221
223
  level: 'error',
222
224
  });
223
225
  throw new Error('Failed to add .gitkeep');