@link-assistant/hive-mind 1.53.0 → 1.53.1
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 +10 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +4 -3
- package/src/github.lib.mjs +59 -43
- package/src/interactive-mode.lib.mjs +12 -2
- package/src/solve.auto-merge-helpers.lib.mjs +9 -6
- package/src/solve.auto-merge.lib.mjs +28 -20
- package/src/solve.mjs +12 -8
- package/src/solve.progress-monitoring.lib.mjs +16 -11
- package/src/solve.repo-setup.lib.mjs +8 -4
- package/src/solve.repository.lib.mjs +6 -4
- package/src/solve.results.lib.mjs +64 -3
- package/src/solve.session.lib.mjs +25 -13
- package/src/solve.watch.lib.mjs +7 -2
- package/src/tool-comments.lib.mjs +271 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.53.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
3
13
|
## 1.53.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -16,6 +16,7 @@ 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';
|
|
@@ -1207,9 +1208,9 @@ export const executeClaudeCommand = async params => {
|
|
|
1207
1208
|
const timeoutType = isActivityTimeout ? 'activity' : 'startup';
|
|
1208
1209
|
const sessionInfo = sessionId ? `\nSession ID: \`${sessionId}\`` : '';
|
|
1209
1210
|
const resumeInfo = isStartupTimeout ? 'Session will be restarted (fresh start).' : `Session will be resumed with \`--resume\` (context preserved).`;
|
|
1210
|
-
const commentBody = `## :warning:
|
|
1211
|
-
|
|
1212
|
-
await log(` Posted force-kill notification to PR #${prNumber}`, { verbose: true });
|
|
1211
|
+
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.*`;
|
|
1212
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
1213
|
+
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
1214
|
} catch (commentError) {
|
|
1214
1215
|
await log(` Warning: Could not post force-kill comment to PR: ${commentError.message}`, { verbose: true });
|
|
1215
1216
|
}
|
package/src/github.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = `## ⏳
|
|
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 = `## 🚨
|
|
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
|
-
|
|
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 = `## ⚠️
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 = `## ⏳
|
|
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 = `## 🚨
|
|
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
|
-
|
|
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 = `## ⚠️
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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: ${
|
|
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 = `## 🤖
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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: ${
|
|
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
|
-
|
|
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 (
|
|
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}: ${
|
|
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 = `## 🚀
|
|
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 = `## 🚀
|
|
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
|
-
// -
|
|
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] &&
|
|
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 = `## 🎉
|
|
267
|
-
await
|
|
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,
|
|
298
|
+
const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber, `## ✅ ${READY_TO_MERGE_MARKER}`, argv.verbose);
|
|
295
299
|
if (hasExistingReadyComment) {
|
|
296
|
-
await log(formatAligned('',
|
|
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 = `## ✅
|
|
302
|
-
|
|
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('',
|
|
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
|
|
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 = `## 🔄
|
|
492
|
-
|
|
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 = `## 🎉
|
|
914
|
-
await
|
|
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 =
|
|
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 = `## ✅
|
|
961
|
-
|
|
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('',
|
|
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 =
|
|
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 = `## ✅
|
|
990
|
-
|
|
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('',
|
|
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
|
package/src/solve.mjs
CHANGED
|
@@ -54,6 +54,10 @@ const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
|
|
|
54
54
|
const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
|
|
55
55
|
const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
|
|
56
56
|
const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
|
|
57
|
+
// Issue #1625: centralized markers + tracked comment posting for solve.mjs's
|
|
58
|
+
// own usage-limit notifications (so they're excluded from the
|
|
59
|
+
// "did the AI post anything?" check in --auto-attach-solution-summary).
|
|
60
|
+
const { postTrackedComment, USAGE_LIMIT_REACHED_MARKER } = await import('./tool-comments.lib.mjs');
|
|
57
61
|
const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
|
|
58
62
|
const { validateAndExitOnInvalidModel } = await import('./models/index.mjs');
|
|
59
63
|
const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
|
|
@@ -964,11 +968,11 @@ try {
|
|
|
964
968
|
// Format the reset time with relative time and UTC conversion if available
|
|
965
969
|
const timezone = global.limitTimezone || null;
|
|
966
970
|
const formattedResetTime = resetTime ? formatResetTimeWithRelative(resetTime, timezone) : null;
|
|
967
|
-
const failureComment = formattedResetTime ? `❌
|
|
971
|
+
const failureComment = formattedResetTime ? `❌ **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. The limit will reset at: **${formattedResetTime}**\n\n${resumeSection}` : `❌ **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. Please wait for the limit to reset.\n\n${resumeSection}`;
|
|
968
972
|
|
|
969
|
-
const
|
|
970
|
-
if (
|
|
971
|
-
await log(
|
|
973
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
|
|
974
|
+
if (posted.ok) {
|
|
975
|
+
await log(` Posted failure comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
972
976
|
}
|
|
973
977
|
} catch (error) {
|
|
974
978
|
await log(` Warning: Could not post failure comment: ${cleanErrorMessage(error)}`, { verbose: true });
|
|
@@ -1049,11 +1053,11 @@ try {
|
|
|
1049
1053
|
// Format reset time with relative time and UTC for better user understanding
|
|
1050
1054
|
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
1051
1055
|
const waitingResetTimeFormatted = formatResetTimeWithRelative(global.limitResetTime, global.limitTimezone || null) || global.limitResetTime;
|
|
1052
|
-
const waitingComment = `⏳
|
|
1056
|
+
const waitingComment = `⏳ **${USAGE_LIMIT_REACHED_MARKER} - Waiting to ${limitContinueMode === 'restart' ? 'Restart' : 'Continue'}**\n\nThe AI tool has reached its usage limit. ${continueModeName} is enabled.\n\n**Reset time:** ${waitingResetTimeFormatted}\n**Wait time:** ${formatWaitTime(waitMs)} (days:hours:minutes:seconds)\n\n${continueDescription}\n\nSession ID: \`${sessionId}\``;
|
|
1053
1057
|
|
|
1054
|
-
const
|
|
1055
|
-
if (
|
|
1056
|
-
await log(
|
|
1058
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: waitingComment });
|
|
1059
|
+
if (posted.ok) {
|
|
1060
|
+
await log(` Posted waiting comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
1057
1061
|
}
|
|
1058
1062
|
} catch (error) {
|
|
1059
1063
|
await log(` Warning: Could not post waiting comment: ${cleanErrorMessage(error)}`, { verbose: true });
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
* @experimental
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
// Issue #1625: centralized markers + tracking helpers so the live-progress
|
|
28
|
+
// comment is excluded from --auto-attach-solution-summary's AI-comment check.
|
|
29
|
+
import { LIVE_PROGRESS_SECTION_START_MARKER, LIVE_PROGRESS_SECTION_END_MARKER, postTrackedCommentFromFile, trackToolCommentId } from './tool-comments.lib.mjs';
|
|
30
|
+
|
|
27
31
|
/**
|
|
28
32
|
* Configuration constants for progress monitoring
|
|
29
33
|
*/
|
|
@@ -33,9 +37,10 @@ const CONFIG = {
|
|
|
33
37
|
// Progress bar characters
|
|
34
38
|
PROGRESS_CHAR_FILLED: '█',
|
|
35
39
|
PROGRESS_CHAR_EMPTY: '░',
|
|
36
|
-
// Marker comments for identifying the progress section
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
// Marker comments for identifying the progress section (imported from
|
|
41
|
+
// tool-comments.lib.mjs — single source of truth).
|
|
42
|
+
PROGRESS_SECTION_START: LIVE_PROGRESS_SECTION_START_MARKER,
|
|
43
|
+
PROGRESS_SECTION_END: LIVE_PROGRESS_SECTION_END_MARKER,
|
|
39
44
|
// Minimum interval between PR description updates (in ms)
|
|
40
45
|
MIN_UPDATE_INTERVAL: 10000, // 10 seconds to avoid rate limiting
|
|
41
46
|
// Valid display modes
|
|
@@ -223,25 +228,25 @@ export const createProgressMonitor = ({ owner, repo, prNumber, $, log, verbose =
|
|
|
223
228
|
await $`gh api repos/${owner}/${repo}/issues/comments/${state.commentId} --method PATCH --field body=@${tempFile}`;
|
|
224
229
|
await fs.unlink(tempFile).catch(() => {});
|
|
225
230
|
} else {
|
|
226
|
-
// Create new comment
|
|
231
|
+
// Create new comment. Issue #1625: post via postTrackedCommentFromFile
|
|
232
|
+
// so the comment ID is captured directly from the GitHub API response
|
|
233
|
+
// (and registered in the in-memory tracking set that excludes tool-
|
|
234
|
+
// posted comments from the "did the AI post anything?" check).
|
|
227
235
|
const fs = (await import('fs')).promises;
|
|
228
236
|
const tempFile = `/tmp/pr-progress-comment-${prNumber}-${Date.now()}.md`;
|
|
229
237
|
await fs.writeFile(tempFile, progressSection);
|
|
230
|
-
const
|
|
238
|
+
const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber: prNumber, bodyFile: tempFile });
|
|
231
239
|
await fs.unlink(tempFile).catch(() => {});
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const output = result.stdout?.toString?.() || '';
|
|
236
|
-
const urlMatch = output.match(/\/comments\/(\d+)/);
|
|
237
|
-
if (urlMatch) {
|
|
238
|
-
state.commentId = urlMatch[1];
|
|
241
|
+
if (posted.ok && posted.commentId) {
|
|
242
|
+
state.commentId = posted.commentId;
|
|
239
243
|
} else {
|
|
240
244
|
// Fallback: find the comment we just created by looking for our marker
|
|
241
245
|
const commentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --jq ${`[.[] | select(.body | contains("${CONFIG.PROGRESS_SECTION_START}")) | .id] | last`}`;
|
|
242
246
|
const commentId = commentsResult.stdout?.toString?.().trim();
|
|
243
247
|
if (commentId && commentId !== 'null') {
|
|
244
248
|
state.commentId = commentId;
|
|
249
|
+
trackToolCommentId(commentId);
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* Handles repository cloning, forking, and remote setup
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Issue #1625: centralized markers + tracked posting for the "empty repo"
|
|
7
|
+
// issue comment so it's excluded from --auto-attach-solution-summary's check.
|
|
8
|
+
import { REPOSITORY_INITIALIZATION_REQUIRED_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
|
|
9
|
+
|
|
6
10
|
export async function setupRepositoryAndClone({ argv, owner, repo, forkOwner, forkRepoName, tempDir, isContinueMode, issueUrl, log, $, needsClone = true }) {
|
|
7
11
|
// Set up repository and handle forking
|
|
8
12
|
const { repoToClone, forkedRepo, upstreamRemote, prForkOwner } = await setupRepository(argv, owner, repo, forkOwner, issueUrl, forkRepoName);
|
|
@@ -223,7 +227,7 @@ async function tryCommentOnIssueAboutEmptyRepo({ issueUrl, owner, repo, log, for
|
|
|
223
227
|
const issueNumber = issueMatch[1];
|
|
224
228
|
await log(`${formatAligned('💬', 'Creating comment:', 'Informing about empty repository...')}`);
|
|
225
229
|
|
|
226
|
-
const commentBody = `## ⚠️
|
|
230
|
+
const commentBody = `## ⚠️ ${REPOSITORY_INITIALIZATION_REQUIRED_MARKER}
|
|
227
231
|
|
|
228
232
|
Hello! I attempted to work on this issue, but encountered a problem:
|
|
229
233
|
|
|
@@ -245,9 +249,9 @@ Once the repository contains at least one commit with any file, I'll be able to
|
|
|
245
249
|
|
|
246
250
|
Thank you!`;
|
|
247
251
|
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`);
|
|
252
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: issueNumber, body: commentBody });
|
|
253
|
+
if (posted.ok) {
|
|
254
|
+
await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`)}`);
|
|
251
255
|
} else {
|
|
252
256
|
await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
|
|
253
257
|
}
|
|
@@ -32,6 +32,8 @@ import { safeExit } from './exit-handler.lib.mjs';
|
|
|
32
32
|
// Import GitHub utilities for permission checks
|
|
33
33
|
const githubLib = await import('./github.lib.mjs');
|
|
34
34
|
const { checkRepositoryWritePermission } = githubLib;
|
|
35
|
+
// Issue #1625: centralized markers + tracked posting.
|
|
36
|
+
const { REPOSITORY_INITIALIZATION_REQUIRED_MARKER, postTrackedComment } = await import('./tool-comments.lib.mjs');
|
|
35
37
|
|
|
36
38
|
// Get root repository (fork source or self), or null if inaccessible
|
|
37
39
|
export const getRootRepository = async (owner, repo) => {
|
|
@@ -698,7 +700,7 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
698
700
|
const issueNumber = issueMatch[1];
|
|
699
701
|
await log(`${formatAligned('💬', 'Creating comment:', 'Requesting maintainer to initialize repository...')}`);
|
|
700
702
|
|
|
701
|
-
const commentBody = `## ⚠️
|
|
703
|
+
const commentBody = `## ⚠️ ${REPOSITORY_INITIALIZATION_REQUIRED_MARKER}
|
|
702
704
|
|
|
703
705
|
Hello! I attempted to work on this issue, but encountered a problem:
|
|
704
706
|
|
|
@@ -717,9 +719,9 @@ Once the repository contains at least one commit with any file, I'll be able to
|
|
|
717
719
|
|
|
718
720
|
Thank you!`;
|
|
719
721
|
|
|
720
|
-
const
|
|
721
|
-
if (
|
|
722
|
-
await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}`)}`);
|
|
722
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: issueNumber, body: commentBody });
|
|
723
|
+
if (posted.ok) {
|
|
724
|
+
await log(`${formatAligned('✅', 'Comment created:', `Posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`)}`);
|
|
723
725
|
} else {
|
|
724
726
|
await log(`${formatAligned('⚠️', 'Note:', 'Could not post comment to issue (this is not critical)')}`);
|
|
725
727
|
}
|
|
@@ -1041,12 +1041,22 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
1041
1041
|
await safeExit(1, 'Execution error');
|
|
1042
1042
|
};
|
|
1043
1043
|
|
|
1044
|
+
// Issue #1625: Markers and in-memory comment-ID tracking are centralized in
|
|
1045
|
+
// src/tool-comments.lib.mjs so that every place that *posts* a tool-generated
|
|
1046
|
+
// comment and the filter that *detects* them share the exact same constants.
|
|
1047
|
+
// Re-exported here for backwards compatibility with imports that expect them
|
|
1048
|
+
// from solve.results.lib.mjs.
|
|
1049
|
+
const toolComments = await import('./tool-comments.lib.mjs');
|
|
1050
|
+
export const { TOOL_GENERATED_COMMENT_MARKERS, isToolGeneratedComment, trackToolCommentId, isToolTrackedCommentId, getTrackedToolCommentIds, postTrackedComment } = toolComments;
|
|
1051
|
+
|
|
1044
1052
|
/**
|
|
1045
1053
|
* Check if new comments were created by the AI during the session.
|
|
1046
1054
|
* This is used by --auto-attach-solution-summary to determine if the AI
|
|
1047
1055
|
* already provided feedback.
|
|
1048
1056
|
*
|
|
1049
1057
|
* Issue #1263: Support for --attach-solution-summary and --auto-attach-solution-summary
|
|
1058
|
+
* Issue #1625: Filter out comments produced by solve.mjs itself (session start,
|
|
1059
|
+
* log upload, auto-restart, etc.) so they do not falsely count as AI-authored.
|
|
1050
1060
|
*
|
|
1051
1061
|
* @param {Date} referenceTime - The timestamp before tool execution
|
|
1052
1062
|
* @param {string} owner - Repository owner
|
|
@@ -1067,13 +1077,62 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
|
|
|
1067
1077
|
return false;
|
|
1068
1078
|
}
|
|
1069
1079
|
|
|
1080
|
+
await log(`🔎 Checking comments by '${currentUser}' after ${referenceTime.toISOString()} (PR #${prNumber ?? 'none'}, issue #${issueNumber ?? 'none'})`, { verbose: true });
|
|
1081
|
+
|
|
1082
|
+
// Issue #1625: A comment counts as an "AI comment" only if it was posted
|
|
1083
|
+
// by the current user AFTER referenceTime AND solve.mjs did NOT post it
|
|
1084
|
+
// itself. We identify tool-posted comments in two ways, in order:
|
|
1085
|
+
// 1. Primary: comment ID is in the in-memory tracked set populated by
|
|
1086
|
+
// every solve.mjs posting site (postTrackedComment / trackToolCommentId).
|
|
1087
|
+
// This is robust to comment-body changes.
|
|
1088
|
+
// 2. Fallback: comment body matches a known TOOL_GENERATED_COMMENT_MARKERS
|
|
1089
|
+
// marker. This catches comments whose IDs weren't captured — for
|
|
1090
|
+
// example, on resumed sessions where the posting happened in an
|
|
1091
|
+
// earlier process, or legacy code paths that predate tracking.
|
|
1092
|
+
// Review-type inline comments cannot be posted by solve.mjs, so they are
|
|
1093
|
+
// treated as AI-authored by default.
|
|
1094
|
+
const filterNewAiComments = (comments, kind) => {
|
|
1095
|
+
const filtered = [];
|
|
1096
|
+
const skippedCounts = {};
|
|
1097
|
+
const skippedByIdCount = { n: 0 };
|
|
1098
|
+
for (const comment of comments) {
|
|
1099
|
+
if (!comment || !comment.user || comment.user.login !== currentUser) continue;
|
|
1100
|
+
if (!(new Date(comment.created_at) > referenceTime)) continue;
|
|
1101
|
+
|
|
1102
|
+
const isReview = kind === 'review';
|
|
1103
|
+
if (!isReview) {
|
|
1104
|
+
if (isToolTrackedCommentId(comment.id)) {
|
|
1105
|
+
skippedByIdCount.n += 1;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (isToolGeneratedComment(comment.body)) {
|
|
1109
|
+
const markerMatch = TOOL_GENERATED_COMMENT_MARKERS.find(m => (comment.body || '').includes(m)) || 'unknown';
|
|
1110
|
+
skippedCounts[markerMatch] = (skippedCounts[markerMatch] || 0) + 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
filtered.push(comment);
|
|
1115
|
+
}
|
|
1116
|
+
if (skippedByIdCount.n > 0) {
|
|
1117
|
+
log(` ⏭️ Skipped ${kind} tool-tracked comment IDs: ${skippedByIdCount.n}`, { verbose: true }).catch(() => {});
|
|
1118
|
+
}
|
|
1119
|
+
if (Object.keys(skippedCounts).length > 0) {
|
|
1120
|
+
const summary = Object.entries(skippedCounts)
|
|
1121
|
+
.map(([m, c]) => `${m}=${c}`)
|
|
1122
|
+
.join(', ');
|
|
1123
|
+
log(` ⏭️ Skipped ${kind} tool-generated comments (marker fallback): ${summary}`, { verbose: true }).catch(() => {});
|
|
1124
|
+
}
|
|
1125
|
+
return filtered;
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1070
1128
|
// Check comments on the PR first (if we have a PR)
|
|
1071
1129
|
if (prNumber) {
|
|
1072
1130
|
// Check PR conversation comments
|
|
1073
1131
|
const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
|
|
1074
1132
|
if (prCommentsResult.code === 0) {
|
|
1075
1133
|
const prComments = JSON.parse(prCommentsResult.stdout.toString().trim() || '[]');
|
|
1076
|
-
const newPrComments = prComments
|
|
1134
|
+
const newPrComments = filterNewAiComments(prComments, 'pr');
|
|
1135
|
+
await log(` 📨 PR conversation comments after referenceTime by '${currentUser}' (excluding tool-generated): ${newPrComments.length}`, { verbose: true });
|
|
1077
1136
|
if (newPrComments.length > 0) {
|
|
1078
1137
|
return true;
|
|
1079
1138
|
}
|
|
@@ -1083,7 +1142,8 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
|
|
|
1083
1142
|
const reviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
|
|
1084
1143
|
if (reviewCommentsResult.code === 0) {
|
|
1085
1144
|
const reviewComments = JSON.parse(reviewCommentsResult.stdout.toString().trim() || '[]');
|
|
1086
|
-
const newReviewComments = reviewComments
|
|
1145
|
+
const newReviewComments = filterNewAiComments(reviewComments, 'review');
|
|
1146
|
+
await log(` 📝 PR review (inline) comments after referenceTime by '${currentUser}': ${newReviewComments.length}`, { verbose: true });
|
|
1087
1147
|
if (newReviewComments.length > 0) {
|
|
1088
1148
|
return true;
|
|
1089
1149
|
}
|
|
@@ -1095,7 +1155,8 @@ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNu
|
|
|
1095
1155
|
const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
|
|
1096
1156
|
if (issueCommentsResult.code === 0) {
|
|
1097
1157
|
const issueComments = JSON.parse(issueCommentsResult.stdout.toString().trim() || '[]');
|
|
1098
|
-
const newIssueComments = issueComments
|
|
1158
|
+
const newIssueComments = filterNewAiComments(issueComments, 'issue');
|
|
1159
|
+
await log(` 📨 Issue comments after referenceTime by '${currentUser}' (excluding tool-generated): ${newIssueComments.length}`, { verbose: true });
|
|
1099
1160
|
if (newIssueComments.length > 0) {
|
|
1100
1161
|
return true;
|
|
1101
1162
|
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* Handles starting and ending work sessions, PR status changes, and session comments
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Issue #1625: Use the single source of truth for session-comment marker
|
|
7
|
+
// strings. Building comment bodies via these constants guarantees the filter
|
|
8
|
+
// in checkForAiCreatedComments() always matches what we actually posted.
|
|
9
|
+
import { AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Session type definitions for different work session contexts
|
|
8
13
|
* See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
@@ -27,26 +32,26 @@ function getSessionCommentContent(sessionType, timestamp) {
|
|
|
27
32
|
case SESSION_TYPES.RESUME:
|
|
28
33
|
return {
|
|
29
34
|
emoji: '🔄',
|
|
30
|
-
header:
|
|
35
|
+
header: AI_WORK_SESSION_RESUMED_MARKER,
|
|
31
36
|
description: `Resuming automated work session at ${isoTime}\n\nThis session continues from a previous session using the \`--resume\` flag.\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This comment marks the resumption of an AI work session. Please wait for the session to finish, and provide your feedback._`,
|
|
32
37
|
};
|
|
33
38
|
case SESSION_TYPES.AUTO_RESUME:
|
|
34
39
|
return {
|
|
35
40
|
emoji: '⏰',
|
|
36
|
-
header:
|
|
41
|
+
header: AUTO_RESUME_ON_LIMIT_RESET_MARKER,
|
|
37
42
|
description: `Auto-resuming automated work session at ${isoTime}\n\nThis session automatically resumed after the usage limit reset, continuing with the previous context preserved.\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This is an auto-resumed session. Please wait for the session to finish, and provide your feedback._`,
|
|
38
43
|
};
|
|
39
44
|
case SESSION_TYPES.AUTO_RESTART:
|
|
40
45
|
return {
|
|
41
46
|
emoji: '🔄',
|
|
42
|
-
header:
|
|
47
|
+
header: AUTO_RESTART_ON_LIMIT_RESET_MARKER,
|
|
43
48
|
description: `Auto-restarting automated work session at ${isoTime}\n\nThis session automatically restarted after the usage limit reset (fresh start without previous context).\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This is a fresh restart after limit reset. Please wait for the session to finish, and provide your feedback._`,
|
|
44
49
|
};
|
|
45
50
|
case SESSION_TYPES.NEW:
|
|
46
51
|
default:
|
|
47
52
|
return {
|
|
48
53
|
emoji: '🤖',
|
|
49
|
-
header:
|
|
54
|
+
header: AI_WORK_SESSION_STARTED_MARKER,
|
|
50
55
|
description: `Starting automated work session at ${isoTime}\n\nThe PR has been converted to draft mode while work is in progress.\n\n_This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback._`,
|
|
51
56
|
};
|
|
52
57
|
}
|
|
@@ -97,13 +102,17 @@ export async function startWorkSession({ isContinueMode, prNumber, argv, log, fo
|
|
|
97
102
|
await log('Warning: Could not check/convert PR draft status', { level: 'warning' });
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
// Post a comment marking the start of work session with appropriate header based on session type
|
|
105
|
+
// Post a comment marking the start of work session with appropriate header based on session type.
|
|
106
|
+
// Issue #1625: Use postTrackedComment so the comment ID is registered in-memory and can be
|
|
107
|
+
// excluded from the "did the AI post anything?" check in checkForAiCreatedComments().
|
|
101
108
|
try {
|
|
102
109
|
const { emoji, header, description } = getSessionCommentContent(sessionType, workStartTime);
|
|
103
110
|
const startComment = `${emoji} **${header}**\n\n${description}`;
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
await log(formatAligned('💬', 'Posted:', `${header} comment`, 2));
|
|
111
|
+
const { ok, commentId, stderr } = await postTrackedComment({ $, owner: global.owner, repo: global.repo, targetNumber: prNumber, body: startComment });
|
|
112
|
+
if (ok) {
|
|
113
|
+
await log(formatAligned('💬', 'Posted:', `${header} comment${commentId ? ` (id=${commentId})` : ''}`, 2));
|
|
114
|
+
} else {
|
|
115
|
+
await log(`Warning: Could not post work start comment: ${stderr || 'unknown error'}`, { level: 'warning' });
|
|
107
116
|
}
|
|
108
117
|
} catch (error) {
|
|
109
118
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
@@ -129,12 +138,15 @@ export async function endWorkSession({ isContinueMode, prNumber, argv, log, form
|
|
|
129
138
|
// Only post end comment if logs were NOT already attached
|
|
130
139
|
// The attachLogToGitHub comment already serves as finishing status with "Now working session is ended" text
|
|
131
140
|
if (!logsAttached) {
|
|
132
|
-
// Post a comment marking the end of work session
|
|
141
|
+
// Post a comment marking the end of work session.
|
|
142
|
+
// Issue #1625: Track the comment ID so it won't be mistaken for AI-authored content.
|
|
133
143
|
try {
|
|
134
|
-
const endComment = `🤖
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
await log(formatAligned('💬', 'Posted:',
|
|
144
|
+
const endComment = `🤖 **${AI_WORK_SESSION_COMPLETED_MARKER}**\n\nWork session ended at ${workEndTime.toISOString()}\n\nThe PR will be converted back to ready for review.\n\n_This comment marks the end of an AI work session. New comments after this time will be considered as feedback._`;
|
|
145
|
+
const { ok, commentId, stderr } = await postTrackedComment({ $, owner: global.owner, repo: global.repo, targetNumber: prNumber, body: endComment });
|
|
146
|
+
if (ok) {
|
|
147
|
+
await log(formatAligned('💬', 'Posted:', `Work session end comment${commentId ? ` (id=${commentId})` : ''}`, 2));
|
|
148
|
+
} else {
|
|
149
|
+
await log(`Warning: Could not post work end comment: ${stderr || 'unknown error'}`, { level: 'warning' });
|
|
138
150
|
}
|
|
139
151
|
} catch (error) {
|
|
140
152
|
const sentryLib = await import('./sentry.lib.mjs');
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -40,6 +40,10 @@ const { checkPRMerged, checkForUncommittedChanges, getUncommittedChangesDetails,
|
|
|
40
40
|
// Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
|
|
41
41
|
const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
|
|
42
42
|
|
|
43
|
+
// Issue #1625: Central marker constants + tracked comment posting
|
|
44
|
+
const toolComments = await import('./tool-comments.lib.mjs');
|
|
45
|
+
const { AUTO_RESTART_MARKER, postTrackedComment } = toolComments;
|
|
46
|
+
|
|
43
47
|
/**
|
|
44
48
|
* Monitor for feedback in a loop and trigger restart when detected
|
|
45
49
|
*/
|
|
@@ -192,8 +196,9 @@ export const watchForFeedback = async params => {
|
|
|
192
196
|
uncommittedFilesList = '\n\n**Uncommitted files:**\n```\n' + changes.join('\n') + '\n```';
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
const commentBody = `## 🔄
|
|
196
|
-
|
|
199
|
+
const commentBody = `## 🔄 ${AUTO_RESTART_MARKER} ${autoRestartCount}/${maxAutoRestartIterations}\n\nDetected uncommitted changes from previous run. Starting new session to review and commit or discard them.${uncommittedFilesList}\n\n---\n*Auto-restart will stop after changes are committed or discarded, or after ${remainingIterations} more iteration${remainingIterations !== 1 ? 's' : ''}. Please wait until working session will end and give your feedback.*`;
|
|
200
|
+
// Issue #1625: Track so this doesn't falsely count as AI-authored.
|
|
201
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
197
202
|
await log(formatAligned('', '💬 Posted auto-restart notification to PR', '', 2));
|
|
198
203
|
} catch (commentError) {
|
|
199
204
|
reportError(commentError, {
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Centralized definitions for GitHub comments posted by solve.mjs itself
|
|
5
|
+
* (session bookkeeping, log uploads, auto-restart notices, etc.) — as
|
|
6
|
+
* opposed to comments posted by the AI agent via its own tool calls.
|
|
7
|
+
*
|
|
8
|
+
* Issue #1625: --auto-attach-solution-summary was broken because the tool's
|
|
9
|
+
* own "AI Work Session Started" / "Solution Draft Log" / "Ready to merge"
|
|
10
|
+
* comments counted as AI-authored comments, so the summary was always
|
|
11
|
+
* suppressed even when the AI session produced zero comments of its own.
|
|
12
|
+
*
|
|
13
|
+
* This module is the single source of truth for the marker strings embedded
|
|
14
|
+
* in those comments. Posting sites use these constants to *build* comment
|
|
15
|
+
* bodies; the summary filter uses the same constants to *detect* them. If a
|
|
16
|
+
* marker needs to change, changing it here updates both sides — no more
|
|
17
|
+
* duplicate literals drifting apart.
|
|
18
|
+
*
|
|
19
|
+
* It also provides in-memory tracking: any comment posted by solve.mjs can
|
|
20
|
+
* be registered by its numeric GitHub comment ID, and checkForAiCreatedComments
|
|
21
|
+
* uses that set as the *primary* filter (marker matching is the fallback for
|
|
22
|
+
* comments whose IDs were not captured, e.g. when `gh pr comment` didn't
|
|
23
|
+
* return JSON).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ----------------------------------------------------------------------------
|
|
27
|
+
// Marker constants — single source of truth for comment header/keyphrase text.
|
|
28
|
+
// Each constant is the exact substring that both (a) appears in the posted
|
|
29
|
+
// comment body and (b) is searched for when filtering out tool-generated
|
|
30
|
+
// comments. Do NOT duplicate these literals elsewhere.
|
|
31
|
+
// ----------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
// solve.session.lib.mjs — startWorkSession() / endWorkSession()
|
|
34
|
+
export const AI_WORK_SESSION_STARTED_MARKER = 'AI Work Session Started';
|
|
35
|
+
export const AI_WORK_SESSION_COMPLETED_MARKER = 'AI Work Session Completed';
|
|
36
|
+
export const AI_WORK_SESSION_RESUMED_MARKER = 'AI Work Session Resumed';
|
|
37
|
+
|
|
38
|
+
// solve.session.lib.mjs — auto-resume / auto-restart on limit reset
|
|
39
|
+
export const AUTO_RESUME_ON_LIMIT_RESET_MARKER = 'Auto Resume (on limit reset)';
|
|
40
|
+
export const AUTO_RESTART_ON_LIMIT_RESET_MARKER = 'Auto Restart (on limit reset)';
|
|
41
|
+
|
|
42
|
+
// github.lib.mjs — attachLogToGitHub() success / resumed / truncated log comments
|
|
43
|
+
export const SOLUTION_DRAFT_LOG_MARKER = 'Solution Draft Log';
|
|
44
|
+
|
|
45
|
+
// solve.watch.lib.mjs / solve.auto-merge.lib.mjs — auto-restart notifications
|
|
46
|
+
export const AUTO_RESTART_MARKER = 'Auto-restart';
|
|
47
|
+
export const AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER = 'Auto-restart-until-mergeable Log';
|
|
48
|
+
|
|
49
|
+
// solve.auto-merge.lib.mjs — "ready to merge" status comments
|
|
50
|
+
export const READY_TO_MERGE_MARKER = 'Ready to merge';
|
|
51
|
+
|
|
52
|
+
// solve.auto-merge.lib.mjs — "auto-merged successfully" status comments
|
|
53
|
+
export const AUTO_MERGED_MARKER = 'Auto-merged';
|
|
54
|
+
|
|
55
|
+
// solve.auto-merge.lib.mjs — billing-limit notification (spending cap / free tier)
|
|
56
|
+
export const BILLING_LIMIT_MARKER = 'GitHub Actions Billing Limit';
|
|
57
|
+
|
|
58
|
+
// github.lib.mjs — fork contributor "Allow edits by maintainers" request
|
|
59
|
+
export const MAINTAINER_ACCESS_REQUEST_MARKER = 'Allow edits by maintainers';
|
|
60
|
+
|
|
61
|
+
// solve.progress-monitoring.lib.mjs — live-progress comment section markers.
|
|
62
|
+
// These are HTML comments so they don't render in the GitHub UI; they exist
|
|
63
|
+
// specifically to let the tool find its own comment later.
|
|
64
|
+
export const LIVE_PROGRESS_SECTION_START_MARKER = '<!-- LIVE-PROGRESS-START -->';
|
|
65
|
+
export const LIVE_PROGRESS_SECTION_END_MARKER = '<!-- LIVE-PROGRESS-END -->';
|
|
66
|
+
|
|
67
|
+
// claude.lib.mjs — "session force-killed due to stream timeout" notifications
|
|
68
|
+
export const SESSION_FORCE_KILLED_MARKER = 'Session Force-Killed';
|
|
69
|
+
|
|
70
|
+
// solve.repo-setup.lib.mjs / solve.repository.lib.mjs — issue comments posted
|
|
71
|
+
// when the target repository is empty / uninitialized so solving can't start.
|
|
72
|
+
export const REPOSITORY_INITIALIZATION_REQUIRED_MARKER = 'Repository Initialization Required';
|
|
73
|
+
|
|
74
|
+
// interactive-mode.lib.mjs — interactive mode session comments
|
|
75
|
+
export const INTERACTIVE_SESSION_STARTED_MARKER = 'Interactive session started';
|
|
76
|
+
export const INTERACTIVE_SESSION_ENDED_MARKER = 'Interactive session ended';
|
|
77
|
+
|
|
78
|
+
// github.lib.mjs — closing footer present in every log upload comment variant
|
|
79
|
+
export const NOW_WORKING_SESSION_IS_ENDED_MARKER = 'Now working session is ended';
|
|
80
|
+
|
|
81
|
+
// Failure-path markers (github.lib.mjs error paths)
|
|
82
|
+
export const SOLUTION_DRAFT_FAILED_MARKER = 'Solution Draft Failed';
|
|
83
|
+
export const SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER = 'Solution Draft Finished with Errors';
|
|
84
|
+
export const USAGE_LIMIT_REACHED_MARKER = 'Usage Limit Reached';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every marker that identifies a tool-posted comment. Derived from the
|
|
88
|
+
* named constants above so that adding a new marker only requires adding
|
|
89
|
+
* the constant and appending it here.
|
|
90
|
+
*/
|
|
91
|
+
export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Markers that indicate the end of a working session. Used by
|
|
95
|
+
* solve.auto-merge-helpers.checkForExistingComment to scope the
|
|
96
|
+
* duplicate-search window to the current session only (Issue #1584).
|
|
97
|
+
*/
|
|
98
|
+
export const SESSION_ENDING_MARKERS = [NOW_WORKING_SESSION_IS_ENDED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Determine whether a GitHub comment body matches any known tool-generated
|
|
102
|
+
* marker. Used as a fallback when a comment's ID was not captured by
|
|
103
|
+
* in-memory tracking (see below).
|
|
104
|
+
*
|
|
105
|
+
* @param {string} body - The comment body
|
|
106
|
+
* @returns {boolean} - True if the body contains a tool-generated marker
|
|
107
|
+
*/
|
|
108
|
+
export const isToolGeneratedComment = body => {
|
|
109
|
+
if (!body || typeof body !== 'string') return false;
|
|
110
|
+
return TOOL_GENERATED_COMMENT_MARKERS.some(marker => body.includes(marker));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ----------------------------------------------------------------------------
|
|
114
|
+
// In-memory tracking of comments posted by solve.mjs during this session.
|
|
115
|
+
//
|
|
116
|
+
// Every tool-initiated comment-post helper should register its comment ID
|
|
117
|
+
// via trackToolCommentId(). checkForAiCreatedComments() then uses the set
|
|
118
|
+
// as the primary filter, falling back to marker-based detection for any
|
|
119
|
+
// comment whose ID was not captured.
|
|
120
|
+
//
|
|
121
|
+
// IDs are GitHub numeric comment IDs (from issue/PR/review comment APIs),
|
|
122
|
+
// coerced to strings for consistent Set membership. Review (inline) comments
|
|
123
|
+
// and conversation comments share the same ID namespace at the API layer,
|
|
124
|
+
// but we never mix them since solve.mjs only posts to conversation + issue
|
|
125
|
+
// endpoints — review comments are AI-only.
|
|
126
|
+
// ----------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
const trackedToolCommentIds = new Set();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Register a comment ID as tool-generated. Safe to call with null/undefined
|
|
132
|
+
* (e.g., when comment posting failed or the ID couldn't be extracted).
|
|
133
|
+
* @param {string|number|null|undefined} commentId
|
|
134
|
+
*/
|
|
135
|
+
export const trackToolCommentId = commentId => {
|
|
136
|
+
if (commentId === null || commentId === undefined) return;
|
|
137
|
+
trackedToolCommentIds.add(String(commentId));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns whether a given comment ID was posted by solve.mjs itself during
|
|
142
|
+
* this session.
|
|
143
|
+
* @param {string|number|null|undefined} commentId
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
export const isToolTrackedCommentId = commentId => {
|
|
147
|
+
if (commentId === null || commentId === undefined) return false;
|
|
148
|
+
return trackedToolCommentIds.has(String(commentId));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns the set of tracked comment IDs (read-only snapshot).
|
|
153
|
+
* Primarily for tests and diagnostics.
|
|
154
|
+
* @returns {Set<string>}
|
|
155
|
+
*/
|
|
156
|
+
export const getTrackedToolCommentIds = () => new Set(trackedToolCommentIds);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reset tracking state. Primarily for tests; solve.mjs does not need to
|
|
160
|
+
* call this between real sessions because each invocation is a fresh
|
|
161
|
+
* process.
|
|
162
|
+
*/
|
|
163
|
+
export const resetTrackedToolCommentIds = () => {
|
|
164
|
+
trackedToolCommentIds.clear();
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Post a GitHub comment on a PR or issue via `gh api` and return the
|
|
169
|
+
* numeric comment ID (as string). The ID is also automatically tracked in
|
|
170
|
+
* the in-memory set above.
|
|
171
|
+
*
|
|
172
|
+
* This is the preferred path for all tool-posted comments because `gh pr
|
|
173
|
+
* comment` / `gh issue comment` only print the comment URL to stdout, and
|
|
174
|
+
* extracting the numeric ID from a URL is brittle. `gh api POST` returns
|
|
175
|
+
* full JSON, from which the ID is trivial to extract.
|
|
176
|
+
*
|
|
177
|
+
* Falls back to best-effort URL parsing if JSON parsing fails, so a single
|
|
178
|
+
* API change cannot break the code path.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} options
|
|
181
|
+
* @param {Function} options.$ - command-stream tagged template (required — we
|
|
182
|
+
* accept it as a parameter so this module has no top-level dependency on
|
|
183
|
+
* `command-stream`, keeping it cheap to import from tests)
|
|
184
|
+
* @param {string} options.owner
|
|
185
|
+
* @param {string} options.repo
|
|
186
|
+
* @param {number|string} options.targetNumber - PR or issue number
|
|
187
|
+
* @param {string} options.body
|
|
188
|
+
* @returns {Promise<{ok: boolean, commentId: string|null, stderr?: string}>}
|
|
189
|
+
*/
|
|
190
|
+
export const postTrackedComment = async ({ $, owner, repo, targetNumber, body }) => {
|
|
191
|
+
if (!$) {
|
|
192
|
+
throw new Error('postTrackedComment requires a command-stream $ helper');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Use `gh api` with stdin to avoid shell-quoting problems on multi-line
|
|
196
|
+
// bodies and to get JSON back so we can extract the comment ID.
|
|
197
|
+
// We use the /issues/<n>/comments endpoint because it works identically
|
|
198
|
+
// for both PRs and issues (a PR is an issue at this endpoint).
|
|
199
|
+
const apiPath = `repos/${owner}/${repo}/issues/${targetNumber}/comments`;
|
|
200
|
+
const payload = JSON.stringify({ body });
|
|
201
|
+
|
|
202
|
+
// `gh api --input -` reads from stdin. command-stream supports .stdin(...)
|
|
203
|
+
// on the returned process handle (same API used in interactive-mode.lib.mjs
|
|
204
|
+
// via execFileAsync). We build the invocation through $ so callers can
|
|
205
|
+
// inject a mock.
|
|
206
|
+
let result;
|
|
207
|
+
try {
|
|
208
|
+
result = await $({ input: payload })`gh api ${apiPath} -X POST --input -`;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { ok: false, commentId: null, stderr: err && err.message ? err.message : String(err) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (result.code !== 0) {
|
|
214
|
+
const stderr = result.stderr ? result.stderr.toString() : '';
|
|
215
|
+
return { ok: false, commentId: null, stderr };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const stdout = result.stdout ? result.stdout.toString() : '';
|
|
219
|
+
let commentId = null;
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(stdout);
|
|
222
|
+
if (parsed && parsed.id !== undefined && parsed.id !== null) {
|
|
223
|
+
commentId = String(parsed.id);
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Fallback: match numeric id in the JSON text, or the issuecomment-<n>
|
|
227
|
+
// fragment in the html_url, whichever shows up first.
|
|
228
|
+
const match = stdout.match(/"id"\s*:\s*(\d+)|issuecomment-(\d+)/);
|
|
229
|
+
if (match) commentId = match[1] || match[2] || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
trackToolCommentId(commentId);
|
|
233
|
+
|
|
234
|
+
return { ok: true, commentId };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Post a GitHub comment whose body is already written to a file on disk.
|
|
239
|
+
* Used by attachLogToGitHub() where the comment body can be tens of KB
|
|
240
|
+
* (entire execution log embedded in a <details>) — too large for inline
|
|
241
|
+
* shell arguments and awkward to pipe as stdin JSON.
|
|
242
|
+
*
|
|
243
|
+
* Reads the file and posts via postTrackedComment() so the returned comment
|
|
244
|
+
* ID is tracked exactly like any other tool-posted comment. Kept separate
|
|
245
|
+
* from postTrackedComment so callers that already have a body string don't
|
|
246
|
+
* pay for a tempfile round-trip.
|
|
247
|
+
*
|
|
248
|
+
* @param {Object} options
|
|
249
|
+
* @param {Function} options.$ - command-stream tagged template
|
|
250
|
+
* @param {string} options.owner
|
|
251
|
+
* @param {string} options.repo
|
|
252
|
+
* @param {number|string} options.targetNumber
|
|
253
|
+
* @param {string} options.bodyFile - absolute path to the comment body file
|
|
254
|
+
* @returns {Promise<{ok: boolean, commentId: string|null, stderr?: string}>}
|
|
255
|
+
*/
|
|
256
|
+
export const postTrackedCommentFromFile = async ({ $, owner, repo, targetNumber, bodyFile }) => {
|
|
257
|
+
if (!$) {
|
|
258
|
+
throw new Error('postTrackedCommentFromFile requires a command-stream $ helper');
|
|
259
|
+
}
|
|
260
|
+
if (typeof globalThis.use === 'undefined') {
|
|
261
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
262
|
+
}
|
|
263
|
+
const fs = (await globalThis.use('fs')).promises;
|
|
264
|
+
let body;
|
|
265
|
+
try {
|
|
266
|
+
body = await fs.readFile(bodyFile, 'utf8');
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return { ok: false, commentId: null, stderr: err && err.message ? err.message : String(err) };
|
|
269
|
+
}
|
|
270
|
+
return postTrackedComment({ $, owner, repo, targetNumber, body });
|
|
271
|
+
};
|