@link-assistant/hive-mind 1.50.3 → 1.50.5

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,20 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 61b2a32: fix: prevent solution draft log and ready to merge comments from appearing between limit reached and auto resume (#1571)
8
+ - `autoContinueWhenLimitResets()` now awaits child process exit instead of returning immediately after spawn
9
+ - Added defense-in-depth guard in solve.mjs to skip post-processing when limit was reached with auto-continue enabled
10
+ - This ensures the correct comment ordering: Limit Reached → Auto Resume → Solution Draft Log → Ready to merge
11
+
12
+ ## 1.50.4
13
+
14
+ ### Patch Changes
15
+
16
+ - 15f25db: Make merge queue cancel immediate during CI waits so users don't have to wait for CI to finish before cancellation takes effect
17
+
3
18
  ## 1.50.3
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.3",
3
+ "version": "1.50.5",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -29,7 +29,7 @@ const exec = promisify(execCallback);
29
29
  * @returns {Promise<{success: boolean, status: string, runs: Array, failedRuns: Array, error: string|null}>}
30
30
  */
31
31
  export async function waitForCommitCI(owner, repo, sha, options = {}, verbose = false) {
32
- const { timeout = 60 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
32
+ const { timeout = 60 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null, isCancelled = null } = options;
33
33
 
34
34
  const startTime = Date.now();
35
35
  let noRunsIterations = 0;
@@ -40,6 +40,9 @@ export async function waitForCommitCI(owner, repo, sha, options = {}, verbose =
40
40
  }
41
41
 
42
42
  while (Date.now() - startTime < timeout) {
43
+ // Issue #1588: Check for cancellation before each poll to allow early exit
44
+ if (isCancelled?.()) return { success: false, status: 'cancelled', runs: [], failedRuns: [], error: 'Operation was cancelled' };
45
+
43
46
  let runs;
44
47
  try {
45
48
  runs = await getWorkflowRunsForSha(owner, repo, sha, verbose);
@@ -16,7 +16,6 @@ import { exec as execCallback } from 'child_process';
16
16
 
17
17
  const exec = promisify(execCallback);
18
18
 
19
- // Import GitHub URL parser
20
19
  import { parseGitHubUrl } from './github.lib.mjs';
21
20
 
22
21
  // Issue #1413: Import ready tag sync, timeline, and label constant from separate module
@@ -728,7 +727,7 @@ export async function getActiveBranchRuns(owner, repo, branch = 'main', verbose
728
727
  * @returns {Promise<{success: boolean, waitedForRuns: boolean, completedRuns: number, error: string|null}>}
729
728
  */
730
729
  export async function waitForBranchCI(owner, repo, branch = 'main', options = {}, verbose = false) {
731
- const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
730
+ const { timeout = 45 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null, isCancelled = null } = options;
732
731
 
733
732
  const startTime = Date.now();
734
733
  let totalWaitedRuns = 0;
@@ -738,6 +737,7 @@ export async function waitForBranchCI(owner, repo, branch = 'main', options = {}
738
737
  }
739
738
 
740
739
  while (Date.now() - startTime < timeout) {
740
+ if (isCancelled?.()) return { success: false, waitedForRuns: totalWaitedRuns > 0, completedRuns: totalWaitedRuns, error: 'Operation was cancelled' };
741
741
  let activeRuns;
742
742
  try {
743
743
  activeRuns = await getActiveBranchRuns(owner, repo, branch, verbose);
@@ -182,8 +182,16 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
182
182
  env: process.env,
183
183
  });
184
184
 
185
- child.on('close', code => {
186
- process.exit(code);
185
+ // Issue #1571: Await child process exit to prevent parent from continuing
186
+ // to post "Solution Draft Log" and "Ready to merge" comments before the
187
+ // resumed session starts. Without this await, the parent process would
188
+ // return from this function and continue executing verifyResults() and
189
+ // startAutoRestartUntilMergeable(), causing confusing comment ordering.
190
+ await new Promise(resolve => {
191
+ child.on('close', code => {
192
+ process.exit(code);
193
+ resolve(); // Won't be reached due to process.exit, but included for completeness
194
+ });
187
195
  });
188
196
  } catch (error) {
189
197
  reportError(error, {
@@ -62,10 +62,7 @@ export const watchUntilMergeable = async params => {
62
62
  const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
63
63
 
64
64
  const rawWatchInterval = argv.watchInterval || 60; // seconds
65
- // Issue #1503: Enforce minimum CI check interval to conserve GitHub API rate limits.
66
- // Issue #1567: Reduced from 5 minutes (300s) to 2 minutes (120s) to decrease wait times
67
- // between working session finish and "Ready to merge" / next action detection.
68
- // This also applies uniformly whether CI/CD is configured or not.
65
+ // Issue #1567: Minimum 120s interval to conserve API rate limits while keeping responsiveness
69
66
  const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
70
67
  const watchInterval = Math.max(rawWatchInterval, MIN_CI_CHECK_INTERVAL_SECONDS);
71
68
  const isAutoMerge = argv.autoMerge || false;
@@ -76,39 +73,19 @@ export const watchUntilMergeable = async params => {
76
73
  let latestSessionId = null;
77
74
  let latestAnthropicCost = null;
78
75
 
79
- // Issue #1323: Track actual restart count separately from check cycle iteration
80
- // `iteration` counts check cycles (how many times we check for blockers)
81
- // `restartCount` counts actual AI tool executions (when we actually restart the AI)
76
+ // Issue #1323: Track actual AI restarts separately from check cycle iterations
82
77
  let restartCount = 0;
83
78
 
84
- // Issue #1371: Track whether a "Ready to merge" comment was posted in THIS session.
85
- // This replaces the all-time history check (checkForExistingComment) which incorrectly
86
- // suppressed new notifications when a previous solve run had already posted one.
87
- // In-memory deduplication correctly handles the case where multiple check cycles in
88
- // the same run detect mergeability simultaneously, without blocking fresh runs.
79
+ // Issue #1371: In-memory dedup for "Ready to merge" comment (per-session, not all-time)
89
80
  let readyToMergeCommentPosted = false;
90
81
 
91
82
  let currentBackoffSeconds = watchInterval;
92
83
 
93
- // Issue #1503: Track consecutive "no workflow runs" checks per-SHA separately from iteration count.
94
- // The `checkCount` parameter in getMergeBlockers is a safety valve that triggers after
95
- // MAX_NO_RUNS_CHECKS (5) consecutive checks with zero workflow runs, concluding CI was
96
- // genuinely not triggered (paths-ignore, fork PRs, etc.). Previously, `iteration` (total
97
- // loop count) was passed as `checkCount`, which meant after 5 iterations (regardless of
98
- // CI state), any new push would immediately trigger the safety valve because checkCount
99
- // was already >= 5. This caused false positive "Ready to merge" when a new commit was
100
- // pushed and CI hadn't registered yet.
101
- //
102
- // Fix: Track the HEAD SHA and reset the counter when it changes (new push detected).
84
+ // Issue #1503: Track consecutive "no workflow runs" checks per-SHA (reset on new push)
103
85
  let consecutiveNoRunsChecks = 0;
104
86
  let lastKnownHeadSha = null;
105
87
 
106
- // Issue #1567: Initial cooldown before first check.
107
- // Wait at least MIN_CI_CHECK_INTERVAL_SECONDS after working session finishes before
108
- // starting to check. This ensures:
109
- // 1. Solution Draft Log is fully posted before any "Ready to merge" can appear
110
- // 2. CI/CD checks have time to register with GitHub (avoids false "no CI" detection)
111
- // 3. Consistent behavior whether CI/CD is configured or not
88
+ // Issue #1567: Initial cooldown to let CI register and solution logs post
112
89
  const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
113
90
 
114
91
  await log('');
@@ -161,9 +138,7 @@ export const watchUntilMergeable = async params => {
161
138
  await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
162
139
 
163
140
  try {
164
- // Issue #1503: Get the current HEAD SHA to detect new pushes and reset the
165
- // consecutive no-runs counter. This prevents false positives where the counter
166
- // from a previous commit's checks carries over to a new commit.
141
+ // Issue #1503: Get current HEAD SHA to detect new pushes and reset no-runs counter
167
142
  let currentHeadSha = null;
168
143
  try {
169
144
  const shaResult = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefOid --jq .headRefOid`;
@@ -184,17 +159,13 @@ export const watchUntilMergeable = async params => {
184
159
  readyToMergeCommentPosted = false;
185
160
  }
186
161
 
187
- // Issue #1503: Increment counter; getMergeBlockers will use it as a safety valve.
188
- // If getMergeBlockers sees no workflow runs on this check, the counter stays incremented.
189
- // If it sees workflow runs or checks, the counter is irrelevant (different code paths).
162
+ // Issue #1503: Increment counter; getMergeBlockers uses it as a safety valve
190
163
  consecutiveNoRunsChecks++;
191
164
 
192
165
  // Get merge blockers
193
166
  const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions, ciStatus } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, consecutiveNoRunsChecks, prBranch);
194
167
 
195
- // Issue #1503: Reset consecutive counter when CI checks or workflow runs were found.
196
- // This ensures the safety valve only fires after truly consecutive "no runs" checks,
197
- // not after interleaved pending/success/failure states that happened to reach the count.
168
+ // Issue #1503: Reset counter when CI checks exist (safety valve only for consecutive "no runs")
198
169
  if (ciStatus && ciStatus.status !== 'no_checks') {
199
170
  // CI checks exist (pending, success, failure, etc.) — the "no runs" counter is irrelevant
200
171
  consecutiveNoRunsChecks = 0;
package/src/solve.mjs CHANGED
@@ -1077,13 +1077,10 @@ try {
1077
1077
  }
1078
1078
  }
1079
1079
 
1080
- // Handle failure cases, but skip exit if limit reached with auto-resume enabled
1081
- // This allows the code to continue to showSessionSummary() where autoContinueWhenLimitResets() is called
1080
+ // Skip failure exit if limit reached with auto-resume (continues to showSessionSummary/autoContinueWhenLimitResets)
1082
1081
  const shouldSkipFailureExitForAutoLimitContinue = limitReached && argv.autoResumeOnLimitReset;
1083
-
1084
1082
  if (!success && !shouldSkipFailureExitForAutoLimitContinue) {
1085
1083
  // Show claude resume command only for --tool claude (or default) on failure
1086
- // Uses the (cd ... && claude --resume ...) pattern for a fully copyable, executable command
1087
1084
  const toolForFailure = argv.tool || 'claude';
1088
1085
  if (sessionId && toolForFailure === 'claude') {
1089
1086
  const claudeResumeCmd = buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model });
@@ -1094,9 +1091,7 @@ try {
1094
1091
  await log('');
1095
1092
  }
1096
1093
 
1097
- // If --attach-logs is enabled, attach failure logs before exiting
1098
- // Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
1099
- // Issues #1212, #1462: Fall back to uploading logs to the issue if PR is not available
1094
+ // Attach failure logs before exiting (Issues #1212, #1462: fall back to issue if no PR)
1100
1095
  const hasPR = global.createdPR && global.createdPR.number;
1101
1096
  const hasIssue = global.issueNumber;
1102
1097
  const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
@@ -1148,8 +1143,7 @@ try {
1148
1143
  await safeExit(1, `${argv.tool.toUpperCase()} execution failed`);
1149
1144
  }
1150
1145
 
1151
- // Clean up .playwright-mcp/ folder before checking for uncommitted changes
1152
- // This prevents browser automation artifacts from triggering auto-restart (Issue #1124)
1146
+ // Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
1153
1147
  if (argv.playwrightMcpAutoCleanup !== false) {
1154
1148
  const playwrightMcpDir = path.join(tempDir, '.playwright-mcp');
1155
1149
  try {
@@ -1179,6 +1173,13 @@ try {
1179
1173
  // Show summary of session and log file
1180
1174
  await showSessionSummary(sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs);
1181
1175
 
1176
+ // Issue #1571: Defense-in-depth guard. autoContinueWhenLimitResets() awaits child exit
1177
+ // and calls process.exit(), so this should not be reached. Skip post-processing to
1178
+ // prevent "Solution Draft Log" / "Ready to merge" comments before "Auto Resume".
1179
+ if (limitReached && (argv.autoResumeOnLimitReset || argv.autoRestartOnLimitReset) && global.limitResetTime) {
1180
+ await safeExit(0, 'Auto-continue child process will handle post-processing');
1181
+ }
1182
+
1182
1183
  // Issue #1263: Handle solution summary attachment
1183
1184
  // --attach-solution-summary: Always attach if result summary is available
1184
1185
  // --auto-attach-solution-summary: Only attach if AI didn't create any comments during session
@@ -238,11 +238,13 @@ export function registerMergeCommand(bot, options) {
238
238
  // Update message with progress and cancel button
239
239
  try {
240
240
  const message = processor.formatProgressMessage();
241
+ // Issue #1588: Do not show cancel button once cancellation has been requested.
242
+ // Without this check, progress updates from CI wait loops would re-add
243
+ // the cancel button after the cancel handler had already removed it.
244
+ const replyMarkup = processor.isCancelled ? undefined : { inline_keyboard: [[{ text: '🛑 Cancel', callback_data: `merge_cancel_${repoKey}` }]] };
241
245
  await ctx.telegram.editMessageText(statusMessage.chat.id, statusMessage.message_id, undefined, message, {
242
246
  parse_mode: 'MarkdownV2',
243
- reply_markup: {
244
- inline_keyboard: [[{ text: '🛑 Cancel', callback_data: `merge_cancel_${repoKey}` }]],
245
- },
247
+ ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
246
248
  });
247
249
  } catch (err) {
248
250
  // Ignore message edit errors (e.g., message not modified)
@@ -513,6 +513,8 @@ export class MergeQueueProcessor {
513
513
  await this.onProgress(this.getProgressUpdate());
514
514
  }
515
515
  },
516
+ // Issue #1588: Pass cancellation check so branch CI wait can abort early
517
+ isCancelled: () => this.isCancelled,
516
518
  },
517
519
  this.verbose
518
520
  );
@@ -617,6 +619,8 @@ export class MergeQueueProcessor {
617
619
  await this.onProgress(this.getProgressUpdate());
618
620
  }
619
621
  },
622
+ // Issue #1588: Pass cancellation check so post-merge CI wait can abort early
623
+ isCancelled: () => this.isCancelled,
620
624
  },
621
625
  this.verbose
622
626
  );