@link-assistant/hive-mind 1.53.0 → 1.54.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,21 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.54.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ee156ba: Disable Claude Code built-in tools and MCP servers that have no value in autonomous headless runs. A new `--useless-tools-disabled` flag (default: `true`, use `--no-useless-tools-disabled` to opt out) adds `AskUserQuestion`, `CronCreate/Delete/List`, `EnterPlanMode/ExitPlanMode`, `EnterWorktree/ExitWorktree`, `Monitor`, `NotebookEdit`, `PushNotification`, `RemoteTrigger`, `ScheduleWakeup` and the three `claude.ai` OAuth MCP connectors (Gmail, Google Drive, Google Calendar) to `--disallowedTools` / `--strict-mcp-config` on each `solve` run. The Docker images (`Dockerfile`, `coolify/Dockerfile`) also bake the same `disallowedTools` list into the baseline `~/.claude/settings.json` so interactive `claude` sessions inside the image don't surface them either (issue #1627).
8
+
9
+ ## 1.53.1
10
+
11
+ ### Patch Changes
12
+
13
+ - c0e8c6d: Fix `--auto-attach-solution-summary` falsely detecting solve.mjs's own session bookkeeping comments ("AI Work Session Started", "Solution Draft Log", "Auto-restart", "Ready to merge", etc.) as AI-authored comments, which caused the solution summary to always be suppressed even when the AI session produced no comments of its own.
14
+
15
+ The fix introduces a new `src/tool-comments.lib.mjs` module as the single source of truth for every marker string embedded in tool-posted comments, along with in-memory tracking of the GitHub comment IDs that solve.mjs itself creates during a session. `checkForAiCreatedComments` now uses the tracked ID set as the primary filter — any comment the tool posted in this session is excluded regardless of body text — and falls back to marker-based substring matching only when an ID was not captured.
16
+
17
+ Every tool-posting site (`solve.session.lib.mjs`, `solve.auto-merge.lib.mjs`, `solve.watch.lib.mjs`, `github.lib.mjs`'s `attachLogToGitHub`/`attachTruncatedLog`/`attachRegularComment`, `claude.lib.mjs`'s force-kill notice, `interactive-mode.lib.mjs`, `solve.progress-monitoring.lib.mjs`, `solve.repo-setup.lib.mjs`, `solve.repository.lib.mjs`, and `solve.mjs`'s usage-limit notifications) now routes through `postTrackedComment` / `postTrackedCommentFromFile`, so every solve-posted comment is registered and filtered correctly across all supported AI tools (claude, codex, agent, opencode). See issue #1625.
18
+
3
19
  ## 1.53.0
4
20
 
5
21
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.53.0",
3
+ "version": "1.54.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",
@@ -16,9 +16,11 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
16
16
  import Decimal from 'decimal.js-light';
17
17
  import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage } from './claude.budget-stats.lib.mjs';
18
18
  import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
19
+ import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
19
20
  import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
20
21
  import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
21
22
  import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
23
+ import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
22
24
  export { availableModels }; // Re-export for backward compatibility
23
25
  const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
24
26
  if (!sessionId || !tempDir) return;
@@ -773,14 +775,9 @@ export const executeClaudeCommand = async params => {
773
775
  await log(`🔄 Resuming from session: ${argv.resume}`);
774
776
  claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
775
777
  }
776
- let mcpConfigPath = null;
777
- if (argv.playwrightMcp === false) {
778
- mcpConfigPath = await buildMcpConfigWithoutPlaywright(log);
779
- if (mcpConfigPath) {
780
- claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
781
- await log('🎭 Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
782
- }
783
- }
778
+ const { mcpConfigPath, disallowedToolsList } = await resolveClaudeSessionToolFlags({ argv, log, fallbackBuildMcpConfigWithoutPlaywright: buildMcpConfigWithoutPlaywright });
779
+ if (mcpConfigPath) claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
780
+ if (disallowedToolsList.length) claudeArgs += ` --disallowedTools ${disallowedToolsList.join(' ')}`;
784
781
  claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
785
782
  const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
786
783
  await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
@@ -805,11 +802,12 @@ export const executeClaudeCommand = async params => {
805
802
  }
806
803
  const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
807
804
  const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
805
+ const disallowedToolsArgs = disallowedToolsList.length ? ['--disallowedTools', ...disallowedToolsList] : [];
808
806
  if (argv.resume) {
809
807
  const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
810
- execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
808
+ execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
811
809
  } else {
812
- execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} --append-system-prompt "${simpleEscapedSystem}"`;
810
+ execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
813
811
  }
814
812
  await log(`${formatAligned('📋', 'Command details:', '')}`);
815
813
  await log(formatAligned('📂', 'Working directory:', tempDir, 2));
@@ -1207,9 +1205,9 @@ export const executeClaudeCommand = async params => {
1207
1205
  const timeoutType = isActivityTimeout ? 'activity' : 'startup';
1208
1206
  const sessionInfo = sessionId ? `\nSession ID: \`${sessionId}\`` : '';
1209
1207
  const resumeInfo = isStartupTimeout ? 'Session will be restarted (fresh start).' : `Session will be resumed with \`--resume\` (context preserved).`;
1210
- const commentBody = `## :warning: Session Force-Killed (${timeoutType} timeout)\n\nThe working session was force-killed due to ${timeoutType} timeout (no stream output for ${isActivityTimeout ? timeouts.streamActivityMs / 1000 : timeouts.streamStartupMs / 1000}s).\n\n**Auto-resuming**: Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}. ${resumeInfo}${sessionInfo}\n\n*This is an automated notification — the session will continue automatically.*`;
1211
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
1212
- await log(` Posted force-kill notification to PR #${prNumber}`, { verbose: true });
1208
+ const commentBody = `## :warning: ${SESSION_FORCE_KILLED_MARKER} (${timeoutType} timeout)\n\nThe working session was force-killed due to ${timeoutType} timeout (no stream output for ${isActivityTimeout ? timeouts.streamActivityMs / 1000 : timeouts.streamStartupMs / 1000}s).\n\n**Auto-resuming**: Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}. ${resumeInfo}${sessionInfo}\n\n*This is an automated notification — the session will continue automatically.*`;
1209
+ const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
1210
+ await log(posted.ok ? ` Posted force-kill notification to PR #${prNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}` : ` Warning: Could not post force-kill comment to PR: ${posted.stderr || 'unknown error'}`, { verbose: true });
1213
1211
  } catch (commentError) {
1214
1212
  await log(` Warning: Could not post force-kill comment to PR: ${commentError.message}`, { verbose: true });
1215
1213
  }
@@ -16,6 +16,9 @@ export { getToolDisplayName }; // Re-export for use by other modules
16
16
  import { buildBudgetStatsString } from './claude.budget-stats.lib.mjs';
17
17
  import { buildCostInfoString } from './github-cost-info.lib.mjs';
18
18
  export { buildCostInfoString };
19
+ // Issue #1625: Named marker constants (single source of truth) + in-memory
20
+ // tracking for tool-posted comments. See tool-comments.lib.mjs for design.
21
+ import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
19
22
  export const maskGitHubToken = maskToken; // Alias for backward compatibility
20
23
  export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
21
24
  export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
@@ -260,13 +263,16 @@ Could you please enable the **"Allow edits by maintainers"** checkbox? This will
260
263
  3. Check the box ✅
261
264
  Alternatively, you can enable it when creating/editing the PR. See: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork
262
265
  Thank you! 🙏`;
263
- const commentResult = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
264
- if (commentResult.code === 0) {
265
- await log('✅ Comment posted successfully', { verbose: true });
266
+ // Issue #1625: track this comment so it's not counted as AI-authored by
267
+ // --auto-attach-solution-summary. The "Allow edits by maintainers"
268
+ // phrase embedded above matches MAINTAINER_ACCESS_REQUEST_MARKER as a
269
+ // fallback if the ID capture fails.
270
+ const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
271
+ if (posted.ok) {
272
+ await log(`✅ Comment posted successfully${posted.commentId ? ` (id=${posted.commentId})` : ''}`, { verbose: true });
266
273
  return true;
267
274
  } else {
268
- const errorOutput = (commentResult.stderr ? commentResult.stderr.toString() : '') + (commentResult.stdout ? commentResult.stdout.toString() : '');
269
- await log(`⚠️ Warning: Failed to post comment: ${cleanErrorMessage(errorOutput)}`, { level: 'warning' });
275
+ await log(`⚠️ Warning: Failed to post comment: ${cleanErrorMessage(posted.stderr || 'unknown error')}`, { level: 'warning' });
270
276
  return false;
271
277
  }
272
278
  } catch (error) {
@@ -295,7 +301,7 @@ export async function attachLogToGitHub(options) {
295
301
  sanitizeLogContent,
296
302
  verbose = false,
297
303
  errorMessage,
298
- customTitle = '🤖 Solution Draft Log',
304
+ customTitle = `🤖 ${SOLUTION_DRAFT_LOG_MARKER}`,
299
305
  sessionId = null,
300
306
  tempDir = null,
301
307
  anthropicTotalCostUSD = null,
@@ -316,7 +322,6 @@ export async function attachLogToGitHub(options) {
316
322
  } = options;
317
323
  const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
318
324
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
319
- const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
320
325
  try {
321
326
  // Issue #1212: Check disk space before attempting log upload (100MB minimum)
322
327
  try {
@@ -411,7 +416,7 @@ export async function attachLogToGitHub(options) {
411
416
  // regardless of whether a generic errorMessage is provided.
412
417
  if (isUsageLimit) {
413
418
  // Usage limit error format - separate from general failures
414
- logComment = `## ⏳ Usage Limit Reached
419
+ logComment = `## ⏳ ${USAGE_LIMIT_REACHED_MARKER}
415
420
 
416
421
  The automated solution draft was interrupted because the ${toolName} usage limit was reached.
417
422
 
@@ -477,7 +482,7 @@ ${logContent}
477
482
  ${footerNote}`;
478
483
  } else if (errorMessage) {
479
484
  // Failure log format (non-usage-limit errors)
480
- logComment = `## 🚨 Solution Draft Failed
485
+ logComment = `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
481
486
  The automated solution draft encountered an error:
482
487
  \`\`\`
483
488
  ${errorMessage}
@@ -493,11 +498,11 @@ ${logContent}
493
498
  </details>
494
499
 
495
500
  ---
496
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
501
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
497
502
  } else if (errorDuringExecution) {
498
503
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
499
504
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
500
- logComment = `## ⚠️ Solution Draft Finished with Errors
505
+ logComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
501
506
  This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
502
507
 
503
508
  > **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
@@ -512,20 +517,23 @@ ${logContent}
512
517
  </details>
513
518
 
514
519
  ---
515
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
520
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
516
521
  } else {
517
522
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
518
523
  // Determine title based on session type (Issue #1152)
524
+ // Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so
525
+ // the filter in checkForAiCreatedComments matches every variant with a
526
+ // single substring check against the centralized marker constant.
519
527
  let title = customTitle;
520
528
  let sessionNote = '';
521
529
  if (sessionType === 'auto-resume') {
522
- title = '🔄 Draft log of auto resume (on limit reset)';
530
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto resume on limit reset)`;
523
531
  sessionNote = '\n\n**Note**: This session was automatically resumed after a usage limit reset, with the previous context preserved.';
524
532
  } else if (sessionType === 'auto-restart') {
525
- title = '🔄 Draft log of auto restart (on limit reset)';
533
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto restart on limit reset)`;
526
534
  sessionNote = '\n\n**Note**: This session was automatically restarted after a usage limit reset (fresh start).';
527
535
  } else if (sessionType === 'resume') {
528
- title = '🔄 Solution Draft Log (Resumed)';
536
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (Resumed)`;
529
537
  sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
530
538
  }
531
539
  logComment = `## ${title}
@@ -541,11 +549,10 @@ ${logContent}
541
549
  </details>
542
550
 
543
551
  ---
544
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
552
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
545
553
  }
546
554
  // Check GitHub comment size limit or large file mode
547
555
  // Issue #1173: Also use gh-upload-log for large files, not just long comments
548
- let commentResult;
549
556
  if (useLargeFileMode || logComment.length > githubLimits.commentMaxSize) {
550
557
  if (useLargeFileMode) {
551
558
  await log(` 📁 Log file too large for inline comment (${Math.round(logStats.size / 1024 / 1024)}MB), using gh-upload-log`);
@@ -604,7 +611,7 @@ ${logContent}
604
611
  // For usage limit cases, always use the dedicated format regardless of errorMessage
605
612
  if (isUsageLimit) {
606
613
  // Usage limit error format
607
- logUploadComment = `## ⏳ Usage Limit Reached
614
+ logUploadComment = `## ⏳ ${USAGE_LIMIT_REACHED_MARKER}
608
615
 
609
616
  The automated solution draft was interrupted because the ${toolName} usage limit was reached.
610
617
 
@@ -664,7 +671,7 @@ ${resumeCommand}
664
671
  ${uploadFooterNote}`;
665
672
  } else if (errorMessage) {
666
673
  // Failure log format (non-usage-limit errors)
667
- logUploadComment = `## 🚨 Solution Draft Failed
674
+ logUploadComment = `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
668
675
  The automated solution draft encountered an error:
669
676
  \`\`\`
670
677
  ${errorMessage}
@@ -674,11 +681,11 @@ ${errorMessage}
674
681
  - [View complete failure log](${logUrl})
675
682
 
676
683
  ---
677
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
684
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
678
685
  } else if (errorDuringExecution) {
679
686
  // Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
680
687
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
681
- logUploadComment = `## ⚠️ Solution Draft Finished with Errors
688
+ logUploadComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
682
689
  This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
683
690
 
684
691
  > **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
@@ -687,22 +694,23 @@ This log file contains the complete execution trace of the AI ${targetType === '
687
694
  - [View complete solution draft log](${logUrl})
688
695
 
689
696
  ---
690
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
697
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
691
698
  } else {
692
699
  // Success log format - use helper function for cost info
693
700
  const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
694
701
  // Determine title based on session type
695
702
  // See: https://github.com/link-assistant/hive-mind/issues/1152
703
+ // Issue #1625: titles embed SOLUTION_DRAFT_LOG_MARKER (single source).
696
704
  let title = customTitle;
697
705
  let sessionNote = '';
698
706
  if (sessionType === 'auto-resume') {
699
- title = '🔄 Draft log of auto resume (on limit reset)';
707
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto resume on limit reset)`;
700
708
  sessionNote = '\n**Note**: This session was automatically resumed after a usage limit reset, with the previous context preserved.\n';
701
709
  } else if (sessionType === 'auto-restart') {
702
- title = '🔄 Draft log of auto restart (on limit reset)';
710
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto restart on limit reset)`;
703
711
  sessionNote = '\n**Note**: This session was automatically restarted after a usage limit reset (fresh start).\n';
704
712
  } else if (sessionType === 'resume') {
705
- title = '🔄 Solution Draft Log (Resumed)';
713
+ title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (Resumed)`;
706
714
  sessionNote = '\n**Note**: This session was manually resumed using the --resume flag.\n';
707
715
  }
708
716
  logUploadComment = `## ${title}
@@ -712,19 +720,22 @@ ${sessionNote}
712
720
  - [View complete solution draft log](${logUrl})
713
721
 
714
722
  ---
715
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
723
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
716
724
  }
717
725
  const tempCommentFile = `/tmp/log-upload-comment-${targetType}-${Date.now()}.md`;
718
726
  await fs.writeFile(tempCommentFile, logUploadComment);
719
- commentResult = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body-file "${tempCommentFile}"`;
727
+ // Issue #1625: post via postTrackedCommentFromFile so the returned
728
+ // comment ID is registered in-memory and excluded from the
729
+ // "did the AI post anything?" check.
730
+ const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempCommentFile });
720
731
  await fs.unlink(tempCommentFile).catch(() => {});
721
- if (commentResult.code === 0) {
722
- await log(` ✅ Solution draft log uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} ${uploadTypeLabel}${chunkInfo}`);
732
+ if (posted.ok) {
733
+ await log(` ✅ Solution draft log uploaded to ${targetName} as ${isPublicRepo ? 'public' : 'private'} ${uploadTypeLabel}${chunkInfo}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
723
734
  await log(` 🔗 Log URL: ${logUrl}`);
724
735
  await log(` 📊 Log size: ${Math.round(logStats.size / 1024)}KB`);
725
736
  return true;
726
737
  } else {
727
- await log(` ❌ Failed to post comment with log link: ${commentResult.stderr ? commentResult.stderr.toString().trim() : 'unknown error'}`);
738
+ await log(` ❌ Failed to post comment with log link: ${posted.stderr || 'unknown error'}`);
728
739
  return false;
729
740
  }
730
741
  } else {
@@ -774,7 +785,7 @@ async function attachTruncatedLog(options) {
774
785
  const maxContentLength = GITHUB_COMMENT_LIMIT - 500;
775
786
  const truncatedContent = logContent.substring(0, maxContentLength) + '\n\n[... Log truncated due to length ...]';
776
787
 
777
- const truncatedComment = `## 🤖 Solution Draft Log (Truncated)
788
+ const truncatedComment = `## 🤖 ${SOLUTION_DRAFT_LOG_MARKER} (Truncated)
778
789
  This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.
779
790
  ⚠️ **Log was truncated** due to GitHub comment size limits.
780
791
 
@@ -788,20 +799,23 @@ ${truncatedContent}
788
799
  </details>
789
800
 
790
801
  ---
791
- *Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
802
+ *${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
792
803
  const tempFile = `/tmp/log-truncated-comment-${targetType}-${Date.now()}.md`;
793
804
  await fs.writeFile(tempFile, truncatedComment);
794
805
 
795
- const result = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body-file "${tempFile}"`;
796
-
806
+ // Issue #1625: track the posted comment ID so it's excluded from the
807
+ // AI-authored-comment check in --auto-attach-solution-summary.
808
+ const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
797
809
  await fs.unlink(tempFile).catch(() => {});
798
-
799
- if (result.code === 0) {
800
- await log(` Truncated solution draft log uploaded to ${targetName}`);
810
+ // ghCommand and targetName are retained in signature for symmetry with
811
+ // attachLogToGitHub's logging vocabulary.
812
+ void ghCommand;
813
+ if (posted.ok) {
814
+ await log(` ✅ Truncated solution draft log uploaded to ${targetName}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
801
815
  await log(` 📊 Log size: ${Math.round(logStats.size / 1024)}KB (truncated)`);
802
816
  return true;
803
817
  } else {
804
- await log(` ❌ Failed to upload truncated log: ${result.stderr ? result.stderr.toString().trim() : 'unknown error'}`);
818
+ await log(` ❌ Failed to upload truncated log: ${posted.stderr || 'unknown error'}`);
805
819
  return false;
806
820
  }
807
821
  }
@@ -814,21 +828,23 @@ async function attachRegularComment(options, logComment) {
814
828
 
815
829
  const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
816
830
  const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
831
+ void ghCommand;
817
832
  const logStats = await fs.stat(logFile);
818
833
 
819
834
  const tempFile = `/tmp/log-comment-${targetType}-${Date.now()}.md`;
820
835
  await fs.writeFile(tempFile, logComment);
821
836
 
822
- const result = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body-file "${tempFile}"`;
823
-
837
+ // Issue #1625: track the posted comment ID so it's excluded from the
838
+ // AI-authored-comment check in --auto-attach-solution-summary.
839
+ const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
824
840
  await fs.unlink(tempFile).catch(() => {});
825
841
 
826
- if (result.code === 0) {
827
- await log(` ✅ Solution draft log uploaded to ${targetName} as comment`);
842
+ if (posted.ok) {
843
+ await log(` ✅ Solution draft log uploaded to ${targetName} as comment${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
828
844
  await log(` 📊 Log size: ${Math.round(logStats.size / 1024)}KB`);
829
845
  return true;
830
846
  } else {
831
- await log(` ❌ Failed to upload log to ${targetName}: ${result.stderr ? result.stderr.toString().trim() : 'unknown error'}`);
847
+ await log(` ❌ Failed to upload log to ${targetName}: ${posted.stderr || 'unknown error'}`);
832
848
  return false;
833
849
  }
834
850
  }
@@ -33,6 +33,11 @@
33
33
  */
34
34
 
35
35
  import { CONFIG, createCollapsible, createRawJsonSection, escapeMarkdown, execFileAsync, formatCost, formatDuration, getToolIcon, safeJsonStringify, sanitizeUnicode, truncateMiddle } from './interactive-mode.shared.lib.mjs';
36
+ // Issue #1625: track interactive-mode comment IDs so they're excluded from
37
+ // the "did the AI post anything?" check in checkForAiCreatedComments().
38
+ // Use the session-started marker as the single source of truth for the
39
+ // header string, keeping posting and filtering in lock-step.
40
+ import { INTERACTIVE_SESSION_STARTED_MARKER, trackToolCommentId } from './tool-comments.lib.mjs';
36
41
 
37
42
  /**
38
43
  * Creates an interactive mode handler for processing Claude/Codex CLI events
@@ -136,6 +141,11 @@ export const createInteractiveHandler = options => {
136
141
  commentId = match ? match[1] || match[2] : null;
137
142
  }
138
143
 
144
+ // Issue #1625: register this comment ID in the shared in-memory tracking
145
+ // set so --auto-attach-solution-summary correctly excludes it from the
146
+ // AI-authored-comment check. Tracking is a no-op when commentId is null.
147
+ trackToolCommentId(commentId);
148
+
139
149
  if (verbose) {
140
150
  await log(`✅ Interactive mode: Comment posted${commentId ? ` (ID: ${commentId})` : ''} (body: ${body.length} chars)`, { verbose: true });
141
151
  }
@@ -288,7 +298,7 @@ export const createInteractiveHandler = options => {
288
298
  const agents = data.agents || [];
289
299
  const agentsList = agents.length > 0 ? agents.map(a => `\`${a}\``).join(', ') : '_None_';
290
300
 
291
- const comment = `## 🚀 Interactive session started
301
+ const comment = `## 🚀 ${INTERACTIVE_SESSION_STARTED_MARKER}
292
302
 
293
303
  | Property | Value |
294
304
  |----------|-------|
@@ -989,7 +999,7 @@ ${createRawJsonSection(data)}`;
989
999
  state.sessionId = data.thread_id || data.session_id || null;
990
1000
  state.startTime = Date.now();
991
1001
 
992
- const comment = `## 🚀 Interactive session started
1002
+ const comment = `## 🚀 ${INTERACTIVE_SESSION_STARTED_MARKER}
993
1003
 
994
1004
  | Property | Value |
995
1005
  |----------|-------|
@@ -35,6 +35,12 @@ const { reportError } = sentryLib;
35
35
  const githubMergeLib = await import('./github-merge.lib.mjs');
36
36
  const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
37
37
 
38
+ // Issue #1625: Import centralized session-ending markers so the duplicate-
39
+ // search scope for checkForExistingComment() stays in lock-step with the
40
+ // markers actually embedded in tool-posted comments.
41
+ const toolComments = await import('./tool-comments.lib.mjs');
42
+ const { SESSION_ENDING_MARKERS } = toolComments;
43
+
38
44
  /**
39
45
  * Issue #1323: Check if a comment with specific content already exists on the PR
40
46
  * This prevents duplicate status comments when multiple processes or restarts occur
@@ -85,14 +91,11 @@ export const checkForExistingComment = async (owner, repo, prNumber, commentSign
85
91
  // Session-ending markers indicate the end of a working session,
86
92
  // so any "Ready to merge" before it belongs to a previous session.
87
93
  //
88
- // Session-ending markers:
89
- // - "Now working session is ended" — in all log upload comments
90
- // (Solution Draft Log, Auto-restart Log, Auto-restart-until-mergeable Log, etc.)
91
- // - "AI Work Session Completed" — posted when logs are not attached
92
- const sessionEndingMarkers = ['Now working session is ended', 'AI Work Session Completed'];
94
+ // Issue #1625: Session-ending markers are now imported from
95
+ // tool-comments.lib.mjs (single source of truth for all markers).
93
96
  let searchStartIndex = 0;
94
97
  for (let i = commentBodies.length - 1; i >= 0; i--) {
95
- if (commentBodies[i] && sessionEndingMarkers.some(marker => commentBodies[i].includes(marker))) {
98
+ if (commentBodies[i] && SESSION_ENDING_MARKERS.some(marker => commentBodies[i].includes(marker))) {
96
99
  searchStartIndex = i + 1;
97
100
  if (verbose) {
98
101
  console.log(`[VERBOSE] Found last session-ending comment at index ${i}, searching from index ${searchStartIndex}`);
@@ -54,6 +54,10 @@ import { limitReset } from './config.lib.mjs';
54
54
  const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
55
55
  const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
56
56
 
57
+ // Issue #1625: Shared marker constants + posting/tracking helpers
58
+ const toolComments = await import('./tool-comments.lib.mjs');
59
+ const { READY_TO_MERGE_MARKER, AUTO_RESTART_MARKER, AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
60
+
57
61
  // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
58
62
  const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
59
63
 
@@ -263,8 +267,8 @@ export const watchUntilMergeable = async params => {
263
267
  try {
264
268
  // Issue #1345: Differentiate message when no CI is configured
265
269
  const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
266
- const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind.\n${ciLine}\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
267
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
270
+ const commentBody = `## 🎉 ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind.\n${ciLine}\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
271
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
268
272
  } catch {
269
273
  // Don't fail if comment posting fails
270
274
  }
@@ -291,19 +295,20 @@ export const watchUntilMergeable = async params => {
291
295
  // Issue #1567: Cross-process deduplication — check if another process already
292
296
  // posted a "Ready to merge" comment. This catches the case where two concurrent
293
297
  // watchUntilMergeable processes both detect mergeability simultaneously.
294
- const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber, '##Ready to merge', argv.verbose);
298
+ const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber, `##${READY_TO_MERGE_MARKER}`, argv.verbose);
295
299
  if (hasExistingReadyComment) {
296
- await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment (already posted by another process)', '', 2));
300
+ await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment (already posted by another process)`, '', 2));
297
301
  readyToMergeCommentPosted = true;
298
302
  } else {
299
303
  // Issue #1345: Differentiate message when no CI is configured
300
304
  const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
301
- const commentBody = `## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
302
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
305
+ const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
306
+ // Issue #1625: Track this comment ID so it can't falsely count as an AI-authored comment
307
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
303
308
  readyToMergeCommentPosted = true;
304
309
  }
305
310
  } else {
306
- await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment (already posted this session)', '', 2));
311
+ await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment (already posted this session)`, '', 2));
307
312
  }
308
313
  } catch {
309
314
  // Don't fail if comment posting fails
@@ -370,7 +375,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
370
375
 
371
376
  ---
372
377
  *Detected by hive-mind with --auto-restart-until-mergeable flag. This is NOT a code issue - human intervention is required.*`;
373
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
378
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
374
379
  await log(formatAligned('', '💬 Posted billing limit notification to PR', '', 2));
375
380
  } catch (commentError) {
376
381
  reportError(commentError, {
@@ -488,8 +493,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
488
493
  // Post a comment to PR about the restart
489
494
  // Issue #1356: Include restart count for tracking and add deduplication
490
495
  try {
491
- const commentBody = `## 🔄 Auto-restart triggered (iteration ${restartCount})\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
492
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
496
+ const commentBody = `## 🔄 ${AUTO_RESTART_MARKER} triggered (iteration ${restartCount})\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
497
+ // Issue #1625: Track so this doesn't falsely count as an AI-authored comment
498
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
493
499
  await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
494
500
  } catch (commentError) {
495
501
  reportError(commentError, {
@@ -910,8 +916,8 @@ export const attemptAutoMerge = async params => {
910
916
 
911
917
  // Post success comment
912
918
  try {
913
- const commentBody = `## 🎉 Auto-merged\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
914
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
919
+ const commentBody = `## 🎉 ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
920
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
915
921
  } catch {
916
922
  // Don't fail if comment posting fails
917
923
  }
@@ -954,14 +960,15 @@ export const startAutoRestartUntilMergeable = async params => {
954
960
 
955
961
  // Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
956
962
  try {
957
- const readyToMergeSignature = '##Ready to merge';
963
+ const readyToMergeSignature = `##${READY_TO_MERGE_MARKER}`;
958
964
  const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
959
965
  if (!hasExistingComment) {
960
- const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
961
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
966
+ const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
967
+ // Issue #1625: Track so this doesn't falsely count as AI-authored.
968
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
962
969
  await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
963
970
  } else {
964
- await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
971
+ await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
965
972
  }
966
973
  } catch {
967
974
  // Don't fail if comment posting fails
@@ -983,14 +990,15 @@ export const startAutoRestartUntilMergeable = async params => {
983
990
 
984
991
  // Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
985
992
  try {
986
- const readyToMergeSignature = '##Ready to merge';
993
+ const readyToMergeSignature = `##${READY_TO_MERGE_MARKER}`;
987
994
  const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
988
995
  if (!hasExistingComment) {
989
- const commentBody = `## ✅ Ready to merge\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
990
- await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
996
+ const commentBody = `## ✅ ${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
997
+ // Issue #1625: Track so this doesn't falsely count as AI-authored.
998
+ await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
991
999
  await log(formatAligned('', '💬 Posted merge readiness notification to PR', '', 2));
992
1000
  } else {
993
- await log(formatAligned('', 'Skipping duplicate "Ready to merge" comment', '', 2));
1001
+ await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
994
1002
  }
995
1003
  } catch {
996
1004
  // Don't fail if comment posting fails
@@ -394,6 +394,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
394
394
  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.',
395
395
  default: true,
396
396
  },
397
+ 'useless-tools-disabled': {
398
+ type: 'boolean',
399
+ description: 'Disable Claude Code built-in tools and MCP servers that have no value (and may be harmful) in autonomous headless runs: AskUserQuestion, CronCreate/Delete/List, EnterPlanMode/ExitPlanMode, EnterWorktree/ExitWorktree, Monitor, NotebookEdit, PushNotification, RemoteTrigger, ScheduleWakeup, and the claude.ai Gmail/Drive/Calendar OAuth connectors. Default: true. Use --no-useless-tools-disabled to keep them enabled. Supported for --tool claude (issue #1627).',
400
+ default: true,
401
+ },
397
402
  'auto-gh-configuration-repair': {
398
403
  type: 'boolean',
399
404
  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.',