@link-assistant/hive-mind 1.49.3 → 1.50.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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 494989e: Add paths filter to CI/CD workflow trigger to skip unnecessary runs for non-code file changes (#1582)
8
+ - c4fadea: fix: prevent push failures in auto-restart and cleanup by syncing with remote (#1572)
9
+ - Add `git pull` before restart sessions and cleanup push to prevent stale local state
10
+ - Add `2>&1` to all `git push` commands so stderr is captured for proper error handling
11
+ - Fix multi-line log message formatting to include timestamps on each line
12
+
13
+ ## 1.50.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 4aed1c1: fix: interactive mode GitHub comments display improvements (#1576)
18
+ - Fix agent task comments stuck at "⏳ Running..." by propagating taskId through comment queue
19
+ - Fix misleading token counts by preferring modelUsage (cumulative per-model) over usage (last-iteration)
20
+ - Change truncation format from "[N lines truncated]" to "[X-Y lines are omitted]" showing actual line range
21
+ - Rename "Session Complete" to "Interactive session completed"
22
+ - Rename Write tool "Content" to "Change", expand by default, add line numbers to diffs
23
+ - Show checked/total count in TodoWrite: "Todos (2/9 items)" instead of "Todos (9 items)"
24
+ - Make Task prompt and Edit Change sections expanded by default
25
+ - Add ToolSearch-specific display with Query/Max Results fields
26
+ - Mark sub-agent tasks with 🤖🔀 emoji and Agent ID field
27
+ - Add queue flushing before waiting for comment IDs in task progress/notification handlers
28
+
3
29
  ## 1.49.3
4
30
 
5
31
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.49.3",
3
+ "version": "1.50.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/agent.lib.mjs CHANGED
@@ -1082,12 +1082,12 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
1082
1082
  if (commitResult.code === 0) {
1083
1083
  await log('✅ Changes committed successfully');
1084
1084
 
1085
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
1085
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
1086
1086
 
1087
1087
  if (pushResult.code === 0) {
1088
1088
  await log('✅ Changes pushed successfully');
1089
1089
  } else {
1090
- await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, {
1090
+ await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
1091
1091
  level: 'warning',
1092
1092
  });
1093
1093
  }
@@ -1449,11 +1449,11 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
1449
1449
  if (commitResult.code === 0) {
1450
1450
  await log('✅ Changes committed successfully');
1451
1451
  await log('📤 Pushing changes to remote...');
1452
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
1452
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
1453
1453
  if (pushResult.code === 0) {
1454
1454
  await log('✅ Changes pushed successfully');
1455
1455
  } else {
1456
- await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, {
1456
+ await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
1457
1457
  level: 'warning',
1458
1458
  });
1459
1459
  }
package/src/codex.lib.mjs CHANGED
@@ -492,12 +492,12 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
492
492
  if (commitResult.code === 0) {
493
493
  await log('✅ Changes committed successfully');
494
494
 
495
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
495
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
496
496
 
497
497
  if (pushResult.code === 0) {
498
498
  await log('✅ Changes pushed successfully');
499
499
  } else {
500
- await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, {
500
+ await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
501
501
  level: 'warning',
502
502
  });
503
503
  }
@@ -143,9 +143,11 @@ const truncateMiddle = (content, options = {}) => {
143
143
 
144
144
  const startLines = lines.slice(0, keepStart);
145
145
  const endLines = lines.slice(-keepEnd);
146
- const removedCount = lines.length - keepStart - keepEnd;
146
+ // Show the actual line number range that was omitted (1-based)
147
+ const omitStart = keepStart + 1;
148
+ const omitEnd = lines.length - keepEnd;
147
149
 
148
- return sanitizeUnicode([...startLines, '', `... [${removedCount} lines truncated] ...`, '', ...endLines].join('\n'));
150
+ return sanitizeUnicode([...startLines, '', `... [${omitStart}-${omitEnd} lines are omitted] ...`, '', ...endLines].join('\n'));
149
151
  };
150
152
 
151
153
  /**
@@ -278,6 +280,7 @@ const getToolIcon = toolName => {
278
280
  WebFetch: '🌐',
279
281
  WebSearch: '🔍',
280
282
  TodoWrite: '📋',
283
+ ToolSearch: '🔍',
281
284
  Task: '🎯',
282
285
  Agent: '🤖',
283
286
  NotebookEdit: '📓',
@@ -339,10 +342,11 @@ export const createInteractiveHandler = options => {
339
342
  * Post a comment to the PR (with rate limiting)
340
343
  * @param {string} body - Comment body
341
344
  * @param {string} [toolId] - Optional tool ID for tracking pending tool calls
345
+ * @param {string} [taskId] - Optional task ID for tracking pending agent tasks
342
346
  * @returns {Promise<string|null>} Comment ID if successful, null if queued or failed
343
347
  * @private
344
348
  */
345
- const postComment = async (body, toolId = null) => {
349
+ const postComment = async (body, toolId = null, taskId = null) => {
346
350
  if (!prNumber || !owner || !repo) {
347
351
  if (verbose) {
348
352
  await log('⚠️ Interactive mode: Cannot post comment - missing PR info', { verbose: true });
@@ -354,10 +358,10 @@ export const createInteractiveHandler = options => {
354
358
  const timeSinceLastComment = now - state.lastCommentTime;
355
359
 
356
360
  if (timeSinceLastComment < CONFIG.MIN_COMMENT_INTERVAL) {
357
- // Queue the comment for later with toolId for tracking
358
- state.commentQueue.push({ body, toolId });
361
+ // Queue the comment for later with toolId/taskId for tracking
362
+ state.commentQueue.push({ body, toolId, taskId });
359
363
  if (verbose) {
360
- await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}`, { verbose: true });
364
+ await log(`📝 Interactive mode: Comment queued (${state.commentQueue.length} in queue)${toolId ? ` [tool: ${toolId}]` : ''}${taskId ? ` [task: ${taskId}]` : ''}`, { verbose: true });
361
365
  }
362
366
  return null;
363
367
  }
@@ -461,8 +465,8 @@ export const createInteractiveHandler = options => {
461
465
 
462
466
  const queueItem = state.commentQueue.shift();
463
467
  if (queueItem) {
464
- const { body, toolId } = queueItem;
465
- // Post the comment (don't pass toolId to avoid re-queueing)
468
+ const { body, toolId, taskId } = queueItem;
469
+ // Post the comment (don't pass toolId/taskId to avoid re-queueing)
466
470
  const commentId = await postComment(body);
467
471
 
468
472
  // If this was a tool use comment, update the pending call with the comment ID
@@ -481,6 +485,25 @@ export const createInteractiveHandler = options => {
481
485
  }
482
486
  }
483
487
  }
488
+
489
+ // If this was a task comment, update the pending task with the comment ID
490
+ // Fix: task comments previously lost their commentId when queued, causing
491
+ // task_notification edits to fail and leaving tasks stuck at "⏳ Running..."
492
+ // See: https://github.com/link-assistant/hive-mind/issues/1576
493
+ if (taskId && commentId) {
494
+ const pendingTask = state.pendingTasks.get(taskId);
495
+ if (pendingTask) {
496
+ pendingTask.commentId = commentId;
497
+ if (pendingTask.resolveCommentId) {
498
+ pendingTask.resolveCommentId(commentId);
499
+ }
500
+ if (verbose) {
501
+ await log(`📋 Interactive mode: Updated pending task ${taskId} with comment ID ${commentId}`, {
502
+ verbose: true,
503
+ });
504
+ }
505
+ }
506
+ }
484
507
  }
485
508
  }
486
509
 
@@ -613,26 +636,26 @@ ${createRawJsonSection(data)}`;
613
636
  keepStart: 12,
614
637
  keepEnd: 12,
615
638
  });
616
- // Format content as diff with + prefix for added lines
639
+ // Format content as diff with + prefix and line numbers for added lines
617
640
  const diffContent = truncatedContent
618
641
  .split('\n')
619
- .map(line => `+ ${line}`)
642
+ .map((line, i) => `+${String(i + 1).padStart(4)} | ${line}`)
620
643
  .join('\n');
621
- inputDisplay += '\n\n' + createCollapsible('📄 Content', '```diff\n' + escapeMarkdown(diffContent) + '\n```');
644
+ inputDisplay += '\n\n' + createCollapsible('📄 Change', '```diff\n' + escapeMarkdown(diffContent) + '\n```', true);
622
645
  }
623
646
  } else if (toolName === 'Edit' && input.file_path) {
624
647
  inputDisplay = `**File:** \`${input.file_path}\``;
625
648
  if (input.old_string && input.new_string) {
626
649
  const truncatedOld = truncateMiddle(input.old_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
627
650
  const truncatedNew = truncateMiddle(input.new_string, { maxLines: 15, keepStart: 6, keepEnd: 6 });
628
- // Format as unified diff with - for removed lines and + for added lines
651
+ // Format as unified diff with - for removed lines and + for added lines, with line numbers
629
652
  const diffOld = truncatedOld
630
653
  .split('\n')
631
- .map(line => `- ${line}`)
654
+ .map((line, i) => `-${String(i + 1).padStart(4)} | ${line}`)
632
655
  .join('\n');
633
656
  const diffNew = truncatedNew
634
657
  .split('\n')
635
- .map(line => `+ ${line}`)
658
+ .map((line, i) => `+${String(i + 1).padStart(4)} | ${line}`)
636
659
  .join('\n');
637
660
  inputDisplay += '\n\n' + createCollapsible('🔄 Change', '```diff\n' + escapeMarkdown(diffOld + '\n' + diffNew) + '\n```', true);
638
661
  }
@@ -665,13 +688,17 @@ ${createRawJsonSection(data)}`;
665
688
  todosPreview = [...startTodos, `- _...and ${skipped} more_`, ...endTodos].join('\n');
666
689
  }
667
690
 
668
- inputDisplay = createCollapsible(`📋 Todos (${todos.length} items)`, todosPreview, true);
691
+ const completedCount = todos.filter(t => t.status === 'completed').length;
692
+ inputDisplay = createCollapsible(`📋 Todos (${completedCount}/${todos.length} items)`, todosPreview, true);
669
693
  } else if (toolName === 'Task') {
670
694
  inputDisplay = `**Description:** ${input.description || 'N/A'}`;
671
695
  if (input.prompt) {
672
696
  const truncatedPrompt = truncateMiddle(input.prompt, { maxLines: 20, keepStart: 8, keepEnd: 8 });
673
- inputDisplay += '\n\n' + createCollapsible('📝 Prompt', truncatedPrompt);
697
+ inputDisplay += '\n\n' + createCollapsible('📝 Prompt', truncatedPrompt, true);
674
698
  }
699
+ } else if (toolName === 'ToolSearch') {
700
+ inputDisplay = `**Query:** \`${input.query || 'N/A'}\``;
701
+ if (input.max_results) inputDisplay += `\n**Max Results:** ${input.max_results}`;
675
702
  } else {
676
703
  // Generic input display
677
704
  const inputJson = truncateMiddle(safeJsonStringify(input, 2), {
@@ -885,7 +912,7 @@ ${createRawJsonSection(data)}`;
885
912
  const handleResult = async data => {
886
913
  const isError = data.is_error || false;
887
914
  const statusIcon = isError ? '❌' : '✅';
888
- const statusText = isError ? 'Session Failed' : 'Session Complete';
915
+ const statusText = isError ? 'Interactive session failed' : 'Interactive session completed';
889
916
 
890
917
  // Format result text
891
918
  const resultText = data.result || '_No result message_';
@@ -913,9 +940,22 @@ ${createRawJsonSection(data)}`;
913
940
  statsTable += `| **Cost** | ${formatCost(data.total_cost_usd)} |\n`;
914
941
  }
915
942
 
916
- // Usage breakdown if available
943
+ // Usage breakdown prefer modelUsage (cumulative per-model totals including sub-agents)
944
+ // over usage (which only contains last-iteration tokens and is misleading).
945
+ // See: https://github.com/link-assistant/hive-mind/issues/1576
917
946
  let usageSection = '';
918
- if (data.usage) {
947
+ if (data.modelUsage && Object.keys(data.modelUsage).length > 0) {
948
+ usageSection = '\n### 📊 Token Usage (by model)\n\n';
949
+ for (const [model, mu] of Object.entries(data.modelUsage)) {
950
+ usageSection += `**${model}:**\n\n| Type | Count |\n|------|-------|\n`;
951
+ if (mu.inputTokens) usageSection += `| Input | ${mu.inputTokens.toLocaleString()} |\n`;
952
+ if (mu.outputTokens) usageSection += `| Output | ${mu.outputTokens.toLocaleString()} |\n`;
953
+ if (mu.cacheCreationInputTokens) usageSection += `| Cache Creation | ${mu.cacheCreationInputTokens.toLocaleString()} |\n`;
954
+ if (mu.cacheReadInputTokens) usageSection += `| Cache Read | ${mu.cacheReadInputTokens.toLocaleString()} |\n`;
955
+ if (typeof mu.costUSD === 'number') usageSection += `| Cost | ${formatCost(mu.costUSD)} |\n`;
956
+ usageSection += '\n';
957
+ }
958
+ } else if (data.usage) {
919
959
  const u = data.usage;
920
960
  usageSection = '\n### 📊 Token Usage\n\n| Type | Count |\n|------|-------|\n';
921
961
  if (u.input_tokens) usageSection += `| Input | ${u.input_tokens.toLocaleString()} |\n`;
@@ -954,6 +994,7 @@ ${createRawJsonSection(data)}`;
954
994
  const toolUseId = data.tool_use_id || '';
955
995
  const description = data.description || 'Agent task';
956
996
  const taskType = data.task_type || 'unknown';
997
+ const agentId = data.agent_id || taskId;
957
998
 
958
999
  // Create a promise for the comment ID (handles queued comments)
959
1000
  let resolveCommentId;
@@ -965,13 +1006,14 @@ ${createRawJsonSection(data)}`;
965
1006
  let promptSection = '';
966
1007
  if (data.prompt) {
967
1008
  const truncatedPrompt = truncateMiddle(data.prompt, { maxLines: 15, keepStart: 6, keepEnd: 6 });
968
- promptSection = '\n\n' + createCollapsible('📝 Task prompt', truncatedPrompt);
1009
+ promptSection = '\n\n' + createCollapsible('📝 Task prompt', truncatedPrompt, true);
969
1010
  }
970
1011
 
971
- const comment = `## 🤖 Agent task: ${escapeMarkdown(description)}
1012
+ const comment = `## 🤖🔀 Agent task: ${escapeMarkdown(description)}
972
1013
 
973
1014
  | Property | Value |
974
1015
  |----------|-------|
1016
+ | **Agent ID** | \`${agentId}\` |
975
1017
  | **Task ID** | \`${taskId || 'unknown'}\` |
976
1018
  | **Type** | \`${taskType}\` |
977
1019
  | **Status** | ⏳ Running... |
@@ -988,12 +1030,13 @@ ${createRawJsonSection(data)}`;
988
1030
  resolveCommentId,
989
1031
  toolUseId,
990
1032
  description,
1033
+ agentId,
991
1034
  lastProgressDescription: description,
992
1035
  progressCount: 0,
993
1036
  allEvents: [data],
994
1037
  });
995
1038
 
996
- const commentId = await postComment(comment, null);
1039
+ const commentId = await postComment(comment, null, taskId);
997
1040
 
998
1041
  if (commentId) {
999
1042
  const pendingTask = state.pendingTasks.get(taskId);
@@ -1028,19 +1071,29 @@ ${createRawJsonSection(data)}`;
1028
1071
 
1029
1072
  let commentId = pendingTask.commentId;
1030
1073
 
1031
- // Wait for comment ID if not yet available
1074
+ // Wait for comment ID if not yet available — flush queue first to avoid timeout
1032
1075
  if (!commentId && pendingTask.commentIdPromise) {
1033
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1034
- commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1076
+ if (state.commentQueue.length > 0) {
1077
+ const wasProcessing = state.isProcessing;
1078
+ state.isProcessing = false;
1079
+ await processQueue();
1080
+ state.isProcessing = wasProcessing;
1081
+ }
1082
+ commentId = pendingTask.commentId;
1083
+ if (!commentId) {
1084
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1085
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1086
+ }
1035
1087
  }
1036
1088
 
1037
1089
  if (commentId) {
1038
- // Build progress steps list from accumulated events
1090
+ // Build progress steps list from accumulated events, marking with agent ID
1091
+ const agentTag = pendingTask.agentId ? `\`[${pendingTask.agentId}]\`` : '';
1039
1092
  const progressSteps = pendingTask.allEvents
1040
1093
  .filter(e => e.subtype === 'task_progress')
1041
1094
  .map(e => {
1042
1095
  const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
1043
- return `- ${toolIcon} ${e.description || 'Working...'}`;
1096
+ return `- 🔀 ${agentTag} ${toolIcon} ${e.description || 'Working...'}`;
1044
1097
  })
1045
1098
  .join('\n');
1046
1099
 
@@ -1048,10 +1101,11 @@ ${createRawJsonSection(data)}`;
1048
1101
  const toolUsesText = usage.tool_uses ? `${usage.tool_uses} tool calls` : '';
1049
1102
  const statsText = [durationText, toolUsesText].filter(Boolean).join(' | ');
1050
1103
 
1051
- const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
1104
+ const updatedComment = `## 🤖🔀 Agent task: ${escapeMarkdown(pendingTask.description)}
1052
1105
 
1053
1106
  | Property | Value |
1054
1107
  |----------|-------|
1108
+ | **Agent ID** | \`${pendingTask.agentId || taskId}\` |
1055
1109
  | **Task ID** | \`${taskId}\` |
1056
1110
  | **Status** | ⏳ Running... |
1057
1111
  | **Progress** | ${pendingTask.progressCount} updates |
@@ -1098,19 +1152,29 @@ ${createRawJsonSection(pendingTask.allEvents.slice(-3))}`;
1098
1152
 
1099
1153
  let commentId = pendingTask.commentId;
1100
1154
 
1101
- // Wait for comment ID if not yet available
1155
+ // Wait for comment ID if not yet available — flush queue first to avoid timeout
1102
1156
  if (!commentId && pendingTask.commentIdPromise) {
1103
- const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1104
- commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1157
+ if (state.commentQueue.length > 0) {
1158
+ const wasProcessing = state.isProcessing;
1159
+ state.isProcessing = false;
1160
+ await processQueue();
1161
+ state.isProcessing = wasProcessing;
1162
+ }
1163
+ commentId = pendingTask.commentId;
1164
+ if (!commentId) {
1165
+ const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(null), 15000));
1166
+ commentId = await Promise.race([pendingTask.commentIdPromise, timeoutPromise]);
1167
+ }
1105
1168
  }
1106
1169
 
1107
1170
  if (commentId) {
1108
- // Build final progress steps list
1171
+ // Build final progress steps list, marking with agent ID
1172
+ const agentTag = pendingTask.agentId ? `\`[${pendingTask.agentId}]\`` : '';
1109
1173
  const progressSteps = pendingTask.allEvents
1110
1174
  .filter(e => e.subtype === 'task_progress')
1111
1175
  .map(e => {
1112
1176
  const toolIcon = e.last_tool_name ? getToolIcon(e.last_tool_name) : '🔄';
1113
- return `- ${toolIcon} ${e.description || 'Working...'}`;
1177
+ return `- 🔀 ${agentTag} ${toolIcon} ${e.description || 'Working...'}`;
1114
1178
  })
1115
1179
  .join('\n');
1116
1180
 
@@ -1119,10 +1183,11 @@ ${createRawJsonSection(pendingTask.allEvents.slice(-3))}`;
1119
1183
  const tokensText = usage.total_tokens ? `${usage.total_tokens.toLocaleString()} tokens` : '';
1120
1184
  const statsText = [durationText, toolUsesText, tokensText].filter(Boolean).join(' | ');
1121
1185
 
1122
- const updatedComment = `## 🤖 Agent task: ${escapeMarkdown(pendingTask.description)}
1186
+ const updatedComment = `## 🤖🔀 Agent task: ${escapeMarkdown(pendingTask.description)}
1123
1187
 
1124
1188
  | Property | Value |
1125
1189
  |----------|-------|
1190
+ | **Agent ID** | \`${pendingTask.agentId || taskId}\` |
1126
1191
  | **Task ID** | \`${taskId}\` |
1127
1192
  | **Status** | ${statusIcon} ${statusText} |
1128
1193
  | **Summary** | ${escapeMarkdown(summary)} |
@@ -1140,8 +1205,11 @@ ${createRawJsonSection([pendingTask.allEvents[0], data])}`;
1140
1205
  state.pendingTasks.delete(taskId);
1141
1206
  } else {
1142
1207
  // Post as standalone if no pending task
1143
- const comment = `## 🤖 Agent task ${statusIcon} ${statusText}
1208
+ const agentId = data.agent_id || taskId;
1209
+ const comment = `## 🤖🔀 Agent task ${statusIcon} ${statusText}
1144
1210
 
1211
+ | **Agent ID** | \`${agentId}\` |
1212
+ |---|---|
1145
1213
  **Summary:** ${escapeMarkdown(summary)}
1146
1214
 
1147
1215
  ---
package/src/lib.mjs CHANGED
@@ -83,8 +83,13 @@ export const log = async (message, options = {}) => {
83
83
  }
84
84
 
85
85
  // Write to file if log file is set
86
+ // Issue #1572: Handle multi-line messages by timestamping each line,
87
+ // so continuation lines don't appear without timestamps in the log file
86
88
  if (logFile) {
87
- const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
89
+ const timestamp = new Date().toISOString();
90
+ const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
91
+ const lines = String(message).split('\n');
92
+ const logMessage = lines.map(line => `${prefix} ${line}`).join('\n');
88
93
  await fs.appendFile(logFile, logMessage + '\n').catch(error => {
89
94
  // Silent fail for file append errors to avoid infinite loop
90
95
  // but report to Sentry in verbose mode
@@ -546,12 +546,12 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
546
546
  if (commitResult.code === 0) {
547
547
  await log('✅ Changes committed successfully');
548
548
 
549
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
549
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
550
550
 
551
551
  if (pushResult.code === 0) {
552
552
  await log('✅ Changes pushed successfully');
553
553
  } else {
554
- await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim()}`, {
554
+ await log(`⚠️ Warning: Could not push changes: ${pushResult.stderr?.toString().trim() || pushResult.stdout?.toString().trim()}`, {
555
555
  level: 'warning',
556
556
  });
557
557
  }
@@ -76,6 +76,14 @@ export const runAutoEnsureRequirements = async ({ issueUrl, owner, repo, issueNu
76
76
  for (let ensureIteration = 1; ensureIteration <= finalizeCount; ensureIteration++) {
77
77
  await log(`🔄 FINALIZE iteration ${ensureIteration}/${finalizeCount}: Restarting to verify requirements...`);
78
78
 
79
+ // Issue #1572: Sync local branch with remote before each finalize iteration
80
+ const pullResult = await $({ cwd: tempDir })`git pull origin ${branchName} 2>&1`;
81
+ if (pullResult.code === 0) {
82
+ await log(` Synced local branch ${branchName} from remote`, { verbose: true });
83
+ } else {
84
+ throw new Error(`git pull failed (code ${pullResult.code}): ${pullResult.stdout || pullResult.stderr || 'no output'}`);
85
+ }
86
+
79
87
  const ensureFeedbackLines = ['', '='.repeat(60), '🔍 FINALIZE REQUIREMENTS CHECK:', '='.repeat(60), '', 'We need to ensure all changes are correct, consistent, validated, tested, logged and fully meet all discussed requirements (check issue description and all comments in issue and in pull request). Ensure all CI/CD checks pass.', ''];
80
88
 
81
89
  const ensureResult = await executeToolIteration({
@@ -976,6 +976,16 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
976
976
  const prStateResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.mergeStateStatus'`;
977
977
  const mergeStateStatus = prStateResult.code === 0 ? prStateResult.stdout.toString().trim() : null;
978
978
 
979
+ // Issue #1572: Sync local branch with remote before restarting to avoid push failures.
980
+ // Without this, the restarted session works on stale local state and can't push.
981
+ const effectiveBranch = prBranch || branchName;
982
+ const pullResult = await $({ cwd: tempDir })`git pull origin ${effectiveBranch} 2>&1`;
983
+ if (pullResult.code === 0) {
984
+ await log(formatAligned('🔄', 'Synced:', `Local branch ${effectiveBranch} updated from remote`));
985
+ } else {
986
+ throw new Error(`git pull failed (code ${pullResult.code}): ${pullResult.stdout || pullResult.stderr || 'no output'}`);
987
+ }
988
+
979
989
  // Execute the AI tool using shared utility
980
990
  await log(formatAligned('🔄', 'Restarting:', `Running ${argv.tool.toUpperCase()} to address issues...`));
981
991
 
package/src/solve.mjs CHANGED
@@ -573,12 +573,12 @@ try {
573
573
  const mergeResult = await $({ cwd: tempDir })`git merge ${defaultBranch} --no-edit`;
574
574
  if (mergeResult.code === 0) {
575
575
  await log(`${formatAligned('✅', 'Merge successful:', 'Pushing merged branch...')}`);
576
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
576
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
577
577
  if (pushResult.code === 0) {
578
578
  await log(`${formatAligned('✅', 'Push successful:', 'Branch updated with latest changes')}`);
579
579
  } else {
580
580
  await log(`${formatAligned('⚠️', 'Push failed:', 'Merge completed but push failed')}`, { level: 'warning' });
581
- await log(` Error: ${pushResult.stderr?.toString() || 'Unknown error'}`, { level: 'warning' });
581
+ await log(` Error: ${pushResult.stderr?.toString() || pushResult.stdout?.toString() || 'Unknown error'}`, { level: 'warning' });
582
582
  }
583
583
  } else {
584
584
  // Merge failed - likely due to conflicts
@@ -1343,13 +1343,13 @@ try {
1343
1343
  await log('');
1344
1344
 
1345
1345
  try {
1346
- const pushResult = await $({ cwd: tempDir })`git push origin ${branchName}`;
1346
+ const pushResult = await $({ cwd: tempDir })`git push origin ${branchName} 2>&1`;
1347
1347
  if (pushResult.code === 0) {
1348
1348
  await log('✅ Changes pushed successfully to remote branch');
1349
1349
  await log(` Branch: ${branchName}`);
1350
1350
  await log('');
1351
1351
  } else {
1352
- const errorMsg = pushResult.stderr?.toString() || 'Unknown error';
1352
+ const errorMsg = pushResult.stderr?.toString() || pushResult.stdout?.toString() || 'Unknown error';
1353
1353
  await log('⚠️ Push failed:', { level: 'error' });
1354
1354
  await log(` ${errorMsg.trim()}`, { level: 'error' });
1355
1355
  await log(' Please push manually:', { level: 'error' });
@@ -1113,12 +1113,12 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
1113
1113
 
1114
1114
  // Step 3: Push the updated default branch to fork to keep it in sync
1115
1115
  await log(`${formatAligned('🔄', 'Pushing to fork:', `${upstreamDefaultBranch} branch`)}`);
1116
- const pushResult = await $({ cwd: tempDir })`git push origin ${upstreamDefaultBranch}`;
1116
+ const pushResult = await $({ cwd: tempDir })`git push origin ${upstreamDefaultBranch} 2>&1`;
1117
1117
  if (pushResult.code === 0) {
1118
1118
  await log(`${formatAligned('✅', 'Fork updated:', 'Default branch pushed to fork')}`);
1119
1119
  } else {
1120
1120
  // Check if it's a non-fast-forward error (fork has diverged from upstream)
1121
- const errorMsg = pushResult.stderr ? pushResult.stderr.toString().trim() : '';
1121
+ const errorMsg = (pushResult.stderr ? pushResult.stderr.toString().trim() : '') || (pushResult.stdout ? pushResult.stdout.toString().trim() : '');
1122
1122
  const isNonFastForward = errorMsg.includes('non-fast-forward') || errorMsg.includes('rejected') || errorMsg.includes('tip of your current branch is behind');
1123
1123
 
1124
1124
  if (isNonFastForward) {
@@ -1147,7 +1147,7 @@ export const setupUpstreamAndSync = async (tempDir, forkedRepo, upstreamRemote,
1147
1147
  await log(`${formatAligned('🔄', 'Force pushing:', 'Syncing fork with upstream (--force-with-lease)')}`);
1148
1148
  const forcePushResult = await $({
1149
1149
  cwd: tempDir,
1150
- })`git push --force-with-lease origin ${upstreamDefaultBranch}`;
1150
+ })`git push --force-with-lease origin ${upstreamDefaultBranch} 2>&1`;
1151
1151
 
1152
1152
  if (forcePushResult.code === 0) {
1153
1153
  await log(`${formatAligned('✅', 'Fork synced:', 'Successfully force-pushed to align with upstream')}`);
@@ -269,6 +269,15 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
269
269
  await log(formatAligned('🔄', 'Cleanup:', `Reverting ${fileName} commit`));
270
270
  await log(` Using saved commit hash: ${claudeCommitHash.substring(0, 7)}...`, { verbose: true });
271
271
 
272
+ // Issue #1572: Sync local branch with remote before cleanup to prevent push failures.
273
+ // After auto-restart sessions, the local branch may be behind the remote.
274
+ const pullResult = await $({ cwd: tempDir })`git pull origin ${branchName} 2>&1`;
275
+ if (pullResult.code === 0) {
276
+ await log(` Synced local branch before cleanup`, { verbose: true });
277
+ } else {
278
+ throw new Error(`git pull failed (code ${pullResult.code}): ${pullResult.stdout || pullResult.stderr || 'no output'}`);
279
+ }
280
+
272
281
  const commitToRevert = claudeCommitHash;
273
282
 
274
283
  // APPROACH 3: Check for modifications before reverting (proactive detection)