@link-assistant/hive-mind 1.21.3 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c000f7b: Add `--attach-solution-summary` and `--auto-attach-solution-summary` options
8
+
9
+ This feature allows users to automatically attach the AI's result summary as a PR/issue comment:
10
+ - `--attach-solution-summary`: Always attach the solution summary when available
11
+ - `--auto-attach-solution-summary`: Only attach the summary if the AI didn't create any comments during the session
12
+
13
+ The solution summary is extracted from the JSON output stream of all AI tools (claude, agent, codex, opencode). Each tool captures the last text content from various JSON event types (text, assistant, message, result) to provide a summary of the work done.
14
+
15
+ Fixes #1263
16
+
17
+ ## 1.21.4
18
+
19
+ ### Patch Changes
20
+
21
+ - ea19c72: Fix queue issues: rejection, display, and formatting
22
+ - Fix disk rejection not blocking queue placement when threshold exceeded
23
+ - Restore "used" label on progress bars when below threshold
24
+ - Show per-queue breakdown in /limits command
25
+ - Group queue items by tool and use human-readable time in /solve_queue
26
+
27
+ - aa42f3a: fix: improve merge queue error handling and debugging (Issue #1269)
28
+ - Always log errors (not just in verbose mode) for critical merge queue failures
29
+ - Always notify users via Telegram when merge queue fails unexpectedly
30
+ - Add timeout wrapper (60s) for onStatusUpdate callback to prevent infinite blocking
31
+ - Add error handling for CI check failures in waitForCI loop
32
+ - Add comprehensive case study documentation in docs/case-studies/issue-1269/
33
+
3
34
  ## 1.21.3
4
35
 
5
36
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.21.3",
3
+ "version": "1.22.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "hive-telegram-bot": "./src/telegram-bot.mjs"
14
14
  },
15
15
  "scripts": {
16
- "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs",
16
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs",
17
17
  "test:queue": "node tests/solve-queue.test.mjs",
18
18
  "test:limits-display": "node tests/limits-display.test.mjs",
19
19
  "test:usage-limit": "node tests/test-usage-limit.mjs",
package/src/agent.lib.mjs CHANGED
@@ -553,6 +553,7 @@ export const executeAgentCommand = async params => {
553
553
  let limitReached = false;
554
554
  let limitResetTime = null;
555
555
  let lastMessage = '';
556
+ let lastTextContent = ''; // Issue #1263: Track last text content for result summary
556
557
  let fullOutput = ''; // Collect all output for error detection (kept for backward compatibility)
557
558
  // Issue #1201: Track error events detected during streaming for reliable error detection
558
559
  // Post-hoc detection on fullOutput can miss errors if NDJSON lines get concatenated without newlines
@@ -613,6 +614,33 @@ export const executeAgentCommand = async params => {
613
614
  streamingErrorMessage = data.message || data.error || line.substring(0, 100);
614
615
  await log(`⚠️ Error event detected in stream: ${streamingErrorMessage}`, { level: 'warning' });
615
616
  }
617
+ // Issue #1263: Track text content for result summary
618
+ // Agent outputs text via 'text', 'assistant', or 'message' type events
619
+ if (data.type === 'text' && data.text) {
620
+ lastTextContent = data.text;
621
+ } else if (data.type === 'assistant' && data.message?.content) {
622
+ // Extract text from assistant message content
623
+ const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
624
+ for (const item of content) {
625
+ if (item.type === 'text' && item.text) {
626
+ lastTextContent = item.text;
627
+ }
628
+ }
629
+ } else if (data.type === 'message' && data.content) {
630
+ // Direct message content
631
+ if (typeof data.content === 'string') {
632
+ lastTextContent = data.content;
633
+ } else if (Array.isArray(data.content)) {
634
+ for (const item of data.content) {
635
+ if (item.type === 'text' && item.text) {
636
+ lastTextContent = item.text;
637
+ }
638
+ }
639
+ }
640
+ } else if (data.type === 'result' && data.result) {
641
+ // Explicit result message (like Claude outputs)
642
+ lastTextContent = data.result;
643
+ }
616
644
  } catch {
617
645
  // Not JSON - log as plain text
618
646
  await log(line);
@@ -647,6 +675,29 @@ export const executeAgentCommand = async params => {
647
675
  streamingErrorMessage = stderrData.message || stderrData.error || stderrLine.substring(0, 100);
648
676
  await log(`⚠️ Error event detected in stream: ${streamingErrorMessage}`, { level: 'warning' });
649
677
  }
678
+ // Issue #1263: Track text content for result summary (stderr)
679
+ if (stderrData.type === 'text' && stderrData.text) {
680
+ lastTextContent = stderrData.text;
681
+ } else if (stderrData.type === 'assistant' && stderrData.message?.content) {
682
+ const content = Array.isArray(stderrData.message.content) ? stderrData.message.content : [stderrData.message.content];
683
+ for (const item of content) {
684
+ if (item.type === 'text' && item.text) {
685
+ lastTextContent = item.text;
686
+ }
687
+ }
688
+ } else if (stderrData.type === 'message' && stderrData.content) {
689
+ if (typeof stderrData.content === 'string') {
690
+ lastTextContent = stderrData.content;
691
+ } else if (Array.isArray(stderrData.content)) {
692
+ for (const item of stderrData.content) {
693
+ if (item.type === 'text' && item.text) {
694
+ lastTextContent = item.text;
695
+ }
696
+ }
697
+ }
698
+ } else if (stderrData.type === 'result' && stderrData.result) {
699
+ lastTextContent = stderrData.result;
700
+ }
650
701
  } catch {
651
702
  // Not JSON - log as plain text
652
703
  await log(stderrLine);
@@ -814,6 +865,7 @@ export const executeAgentCommand = async params => {
814
865
  tokenUsage,
815
866
  pricingInfo,
816
867
  publicPricingEstimate: pricingInfo.totalCostUSD,
868
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
817
869
  };
818
870
  }
819
871
 
@@ -867,6 +919,11 @@ export const executeAgentCommand = async params => {
867
919
  }
868
920
  }
869
921
 
922
+ // Issue #1263: Log if result summary was captured
923
+ if (lastTextContent) {
924
+ await log('📝 Captured result summary from Agent output', { verbose: true });
925
+ }
926
+
870
927
  return {
871
928
  success: true,
872
929
  sessionId,
@@ -875,6 +932,7 @@ export const executeAgentCommand = async params => {
875
932
  tokenUsage,
876
933
  pricingInfo,
877
934
  publicPricingEstimate: pricingInfo.totalCostUSD,
935
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
878
936
  };
879
937
  } catch (error) {
880
938
  reportError(error, {
@@ -893,6 +951,7 @@ export const executeAgentCommand = async params => {
893
951
  tokenUsage: null,
894
952
  pricingInfo: null,
895
953
  publicPricingEstimate: null,
954
+ resultSummary: null, // Issue #1263: No result summary available on error
896
955
  };
897
956
  }
898
957
  };
@@ -861,6 +861,7 @@ export const executeClaudeCommand = async params => {
861
861
  let stderrErrors = [];
862
862
  let anthropicTotalCostUSD = null; // Capture Anthropic's official total_cost_usd from result
863
863
  let errorDuringExecution = false; // Issue #1088: Track if error_during_execution subtype occurred
864
+ let resultSummary = null; // Issue #1263: Capture AI result summary for --attach-solution-summary
864
865
 
865
866
  // Create interactive mode handler if enabled
866
867
  let interactiveHandler = null;
@@ -901,12 +902,10 @@ export const executeClaudeCommand = async params => {
901
902
  await log('---BEGIN USER PROMPT---', { verbose: true });
902
903
  await log(prompt, { verbose: true });
903
904
  await log('---END USER PROMPT---', { verbose: true });
904
- await log('', { verbose: true });
905
905
  await log('📋 System prompt:', { verbose: true });
906
906
  await log('---BEGIN SYSTEM PROMPT---', { verbose: true });
907
907
  await log(systemPrompt, { verbose: true });
908
908
  await log('---END SYSTEM PROMPT---', { verbose: true });
909
- await log('', { verbose: true });
910
909
  }
911
910
  try {
912
911
  // Resolve thinking settings (handles translation between --think and --thinking-budget based on Claude version)
@@ -1001,6 +1000,12 @@ export const executeClaudeCommand = async params => {
1001
1000
  } else if (data.total_cost_usd !== undefined && data.total_cost_usd !== null) {
1002
1001
  await log(`💰 Anthropic cost from ${data.subtype || 'unknown'} result ignored: $${data.total_cost_usd.toFixed(6)}`, { verbose: true });
1003
1002
  }
1003
+ // Issue #1263: Extract result summary for --attach-solution-summary and --auto-attach-solution-summary
1004
+ // The result field contains the AI's summary of the work done
1005
+ if (data.subtype === 'success' && data.result && typeof data.result === 'string') {
1006
+ resultSummary = data.result;
1007
+ await log('📝 Captured result summary from Claude output', { verbose: true });
1008
+ }
1004
1009
  if (data.is_error === true) {
1005
1010
  lastMessage = data.result || JSON.stringify(data);
1006
1011
  const subtype = data.subtype || 'unknown';
@@ -1067,8 +1072,7 @@ export const executeClaudeCommand = async params => {
1067
1072
  const termsAcceptancePattern = /\[ACTION REQUIRED\].*terms|must run.*claude.*review.*terms/i;
1068
1073
  if (termsAcceptancePattern.test(line)) {
1069
1074
  commandFailed = true;
1070
- await log('\n❌ Claude Code requires terms acceptance - please run `claude` interactively to accept the updated terms', { level: 'error' });
1071
- await log(' This is not an error in your code, but Claude CLI needs human interaction.', { level: 'error' });
1075
+ await log('\n❌ Claude Code requires terms acceptance - please run `claude` interactively to accept the updated terms\n This is not an error in your code, but Claude CLI needs human interaction.', { level: 'error' });
1072
1076
  }
1073
1077
  }
1074
1078
  }
@@ -1125,8 +1129,7 @@ export const executeClaudeCommand = async params => {
1125
1129
  // Specifically detect "command not found" via exit code 127
1126
1130
  if (resultExitCode === 127 && !commandFailed) {
1127
1131
  commandFailed = true;
1128
- await log(`\n❌ Command not found (exit code 127) - "${claudePath}" is not installed or not in PATH`, { level: 'error' });
1129
- await log(' Please ensure Claude CLI is installed: npm install -g @anthropic-ai/claude-code', { level: 'error' });
1132
+ await log(`\n❌ Command not found (exit code 127) - "${claudePath}" is not installed or not in PATH\n Please ensure Claude CLI is installed: npm install -g @anthropic-ai/claude-code`, { level: 'error' });
1130
1133
  }
1131
1134
  }
1132
1135
 
@@ -1151,8 +1154,7 @@ export const executeClaudeCommand = async params => {
1151
1154
  retryCount++;
1152
1155
  return await executeWithRetry();
1153
1156
  } else {
1154
- await log(`\n\n❌ API overload error persisted after ${maxRetries} retries`, { level: 'error' });
1155
- await log(' The API appears to be heavily loaded. Please try again later.', { level: 'error' });
1157
+ await log(`\n\n❌ API overload error persisted after ${maxRetries} retries\n The API appears to be heavily loaded. Please try again later.`, { level: 'error' });
1156
1158
  return {
1157
1159
  success: false,
1158
1160
  sessionId,
@@ -1162,6 +1164,7 @@ export const executeClaudeCommand = async params => {
1162
1164
  messageCount,
1163
1165
  toolUseCount,
1164
1166
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1167
+ resultSummary, // Issue #1263: Include result summary
1165
1168
  };
1166
1169
  }
1167
1170
  }
@@ -1196,11 +1199,7 @@ export const executeClaudeCommand = async params => {
1196
1199
  retryCount++;
1197
1200
  return await executeWithRetry();
1198
1201
  } else {
1199
- await log(`\n\n❌ 503 network error persisted after ${retryLimits.max503Retries} retries`, {
1200
- level: 'error',
1201
- });
1202
- await log(' The Anthropic API appears to be experiencing network issues.', { level: 'error' });
1203
- await log(' Please try again later or check https://status.anthropic.com/', { level: 'error' });
1202
+ await log(`\n\n❌ 503 network error persisted after ${retryLimits.max503Retries} retries\n The Anthropic API appears to be experiencing network issues.\n Please try again later or check https://status.anthropic.com/`, { level: 'error' });
1204
1203
  return {
1205
1204
  success: false,
1206
1205
  sessionId,
@@ -1211,6 +1210,7 @@ export const executeClaudeCommand = async params => {
1211
1210
  toolUseCount,
1212
1211
  is503Error: true,
1213
1212
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1213
+ resultSummary, // Issue #1263: Include result summary
1214
1214
  };
1215
1215
  }
1216
1216
  }
@@ -1265,11 +1265,11 @@ export const executeClaudeCommand = async params => {
1265
1265
  // See: docs/dependencies-research/claude-code-issues/README.md for full details
1266
1266
  if (!commandFailed && stderrErrors.length > 0 && messageCount === 0 && toolUseCount === 0) {
1267
1267
  commandFailed = true;
1268
- await log('\n\n❌ Command failed: No messages processed and errors detected in stderr', { level: 'error' });
1269
- await log('Stderr errors:', { level: 'error' });
1270
- for (const err of stderrErrors.slice(0, 5)) {
1271
- await log(` ${err.substring(0, 200)}`, { level: 'error' });
1272
- }
1268
+ const errorsPreview = stderrErrors
1269
+ .slice(0, 5)
1270
+ .map(e => ` ${e.substring(0, 200)}`)
1271
+ .join('\n');
1272
+ await log(`\n\n❌ Command failed: No messages processed and errors detected in stderr\nStderr errors:\n${errorsPreview}`, { level: 'error' });
1273
1273
  }
1274
1274
  if (commandFailed) {
1275
1275
  // Take resource snapshot after failure
@@ -1277,8 +1277,6 @@ export const executeClaudeCommand = async params => {
1277
1277
  await log('\n📈 System resources after execution:', { verbose: true });
1278
1278
  await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
1279
1279
  await log(` Load: ${resourcesAfter.load}`, { verbose: true });
1280
- // Log attachment will be handled by solve.mjs when it receives success=false
1281
- await log('', { verbose: true });
1282
1280
  await showResumeCommand(sessionId, tempDir, claudePath, argv.model, log);
1283
1281
  return {
1284
1282
  success: false,
@@ -1290,6 +1288,7 @@ export const executeClaudeCommand = async params => {
1290
1288
  toolUseCount,
1291
1289
  errorDuringExecution,
1292
1290
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1291
+ resultSummary, // Issue #1263: Include result summary
1293
1292
  };
1294
1293
  }
1295
1294
  // Issue #1088: If error_during_execution occurred but command didn't fail,
@@ -1361,6 +1360,7 @@ export const executeClaudeCommand = async params => {
1361
1360
  toolUseCount,
1362
1361
  anthropicTotalCostUSD, // Pass Anthropic's official total cost
1363
1362
  errorDuringExecution, // Issue #1088: Track if error_during_execution subtype occurred
1363
+ resultSummary, // Issue #1263: Include result summary for --attach-solution-summary
1364
1364
  };
1365
1365
  } catch (error) {
1366
1366
  reportError(error, {
@@ -1409,6 +1409,7 @@ export const executeClaudeCommand = async params => {
1409
1409
  messageCount,
1410
1410
  toolUseCount,
1411
1411
  anthropicTotalCostUSD, // Issue #1104: Include cost even on failure
1412
+ resultSummary, // Issue #1263: Include result summary
1412
1413
  };
1413
1414
  }
1414
1415
  }; // End of executeWithRetry function
package/src/codex.lib.mjs CHANGED
@@ -288,6 +288,7 @@ export const executeCodexCommand = async params => {
288
288
  let limitReached = false;
289
289
  let limitResetTime = null;
290
290
  let lastMessage = '';
291
+ let lastTextContent = ''; // Issue #1263: Track last text content for result summary
291
292
  let authError = false;
292
293
 
293
294
  for await (const chunk of execCommand.stream()) {
@@ -325,6 +326,31 @@ export const executeCodexCommand = async params => {
325
326
  await log(' This error cannot be resolved by retrying.', { level: 'error' });
326
327
  await log(' 💡 Please run: codex login', { level: 'error' });
327
328
  }
329
+
330
+ // Issue #1263: Track text content for result summary
331
+ // Codex outputs text via 'text', 'assistant', 'message', or 'result' type events
332
+ if (data.type === 'text' && data.text) {
333
+ lastTextContent = data.text;
334
+ } else if (data.type === 'assistant' && data.message?.content) {
335
+ const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
336
+ for (const item of content) {
337
+ if (item.type === 'text' && item.text) {
338
+ lastTextContent = item.text;
339
+ }
340
+ }
341
+ } else if (data.type === 'message' && data.content) {
342
+ if (typeof data.content === 'string') {
343
+ lastTextContent = data.content;
344
+ } else if (Array.isArray(data.content)) {
345
+ for (const item of data.content) {
346
+ if (item.type === 'text' && item.text) {
347
+ lastTextContent = item.text;
348
+ }
349
+ }
350
+ }
351
+ } else if (data.type === 'result' && data.result) {
352
+ lastTextContent = data.result;
353
+ }
328
354
  }
329
355
  } catch {
330
356
  // Not JSON, continue
@@ -386,16 +412,23 @@ export const executeCodexCommand = async params => {
386
412
  sessionId,
387
413
  limitReached,
388
414
  limitResetTime,
415
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
389
416
  };
390
417
  }
391
418
 
392
419
  await log('\n\n✅ Codex command completed');
393
420
 
421
+ // Issue #1263: Log if result summary was captured
422
+ if (lastTextContent) {
423
+ await log('📝 Captured result summary from Codex output', { verbose: true });
424
+ }
425
+
394
426
  return {
395
427
  success: true,
396
428
  sessionId,
397
429
  limitReached,
398
430
  limitResetTime,
431
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
399
432
  };
400
433
  } catch (error) {
401
434
  // Don't report auth errors to Sentry as they are user configuration issues
@@ -420,6 +453,7 @@ export const executeCodexCommand = async params => {
420
453
  sessionId: null,
421
454
  limitReached: false,
422
455
  limitResetTime: null,
456
+ resultSummary: null, // Issue #1263: No result summary available on error
423
457
  };
424
458
  }
425
459
  };
@@ -485,15 +485,39 @@ export async function mergePullRequest(owner, repo, prNumber, options = {}, verb
485
485
  * @returns {Promise<{success: boolean, status: string, error: string|null}>}
486
486
  */
487
487
  export async function waitForCI(owner, repo, prNumber, options = {}, verbose = false) {
488
- const { timeout = 30 * 60 * 1000, pollInterval = 30 * 1000, onStatusUpdate = null } = options;
488
+ const {
489
+ timeout = 30 * 60 * 1000,
490
+ pollInterval = 30 * 1000,
491
+ onStatusUpdate = null,
492
+ // Issue #1269: Add timeout for callback to prevent infinite blocking
493
+ callbackTimeout = 60 * 1000, // 1 minute max for callback
494
+ } = options;
489
495
 
490
496
  const startTime = Date.now();
491
497
 
492
498
  while (Date.now() - startTime < timeout) {
493
- const ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
499
+ let ciStatus;
500
+ try {
501
+ ciStatus = await checkPRCIStatus(owner, repo, prNumber, verbose);
502
+ } catch (error) {
503
+ // Issue #1269: Log and continue on CI check errors instead of crashing
504
+ console.error(`[ERROR] /merge: Error checking CI status for PR #${prNumber}: ${error.message}`);
505
+ verbose && console.error(`[VERBOSE] /merge: CI check error details:`, error);
506
+ // Wait and retry
507
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
508
+ continue;
509
+ }
494
510
 
495
511
  if (onStatusUpdate) {
496
- await onStatusUpdate(ciStatus);
512
+ // Issue #1269: Wrap callback with timeout to prevent infinite blocking
513
+ try {
514
+ await Promise.race([onStatusUpdate(ciStatus), new Promise((_, reject) => setTimeout(() => reject(new Error(`Callback timeout after ${callbackTimeout}ms`)), callbackTimeout))]);
515
+ } catch (callbackError) {
516
+ // Issue #1269: Log callback errors but continue processing
517
+ console.error(`[ERROR] /merge: Status update callback failed for PR #${prNumber}: ${callbackError.message}`);
518
+ verbose && console.error(`[VERBOSE] /merge: Callback error details:`, callbackError);
519
+ // Continue processing even if callback fails - don't let UI issues block merging
520
+ }
497
521
  }
498
522
 
499
523
  if (ciStatus.status === 'success') {
@@ -714,11 +714,13 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
714
714
  if (cpuLoad) {
715
715
  message += 'CPU\n';
716
716
  const usedBar = getProgressBar(cpuLoad.usagePercentage, DISPLAY_THRESHOLDS.CPU);
717
- const warning = cpuLoad.usagePercentage >= DISPLAY_THRESHOLDS.CPU ? ' ⚠️' : '';
718
- message += `${usedBar} ${cpuLoad.usagePercentage}%${warning}\n`;
717
+ // Show 'used' label when below threshold, warning emoji when at/above threshold
718
+ // See: https://github.com/link-assistant/hive-mind/issues/1267
719
+ const suffix = cpuLoad.usagePercentage >= DISPLAY_THRESHOLDS.CPU ? ' ⚠️' : ' used';
720
+ message += `${usedBar} ${cpuLoad.usagePercentage}%${suffix}\n`;
719
721
  // Show cores used based on 5m load average (e.g., "0.04/6 CPU cores used" or "3/6 CPU cores used")
720
722
  // Use parseFloat to strip unnecessary trailing zeros (3.00 -> 3, 0.10 -> 0.1, 0.04 -> 0.04)
721
- message += `${parseFloat(cpuLoad.loadAvg5.toFixed(2))}/${cpuLoad.cpuCount} CPU cores used\n\n`;
723
+ message += `${parseFloat(cpuLoad.loadAvg5.toFixed(2))}/${cpuLoad.cpuCount} CPU cores\n\n`;
722
724
  }
723
725
 
724
726
  // Memory section (if provided)
@@ -726,8 +728,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
726
728
  if (memory) {
727
729
  message += 'RAM\n';
728
730
  const usedBar = getProgressBar(memory.usedPercentage, DISPLAY_THRESHOLDS.RAM);
729
- const warning = memory.usedPercentage >= DISPLAY_THRESHOLDS.RAM ? ' ⚠️' : '';
730
- message += `${usedBar} ${memory.usedPercentage}%${warning}\n`;
731
+ const suffix = memory.usedPercentage >= DISPLAY_THRESHOLDS.RAM ? ' ⚠️' : ' used';
732
+ message += `${usedBar} ${memory.usedPercentage}%${suffix}\n`;
731
733
  message += `${formatBytesRange(memory.usedBytes, memory.totalBytes)}\n\n`;
732
734
  }
733
735
 
@@ -737,8 +739,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
737
739
  message += 'Disk space\n';
738
740
  // Show used percentage with progress bar and threshold marker
739
741
  const usedBar = getProgressBar(diskSpace.usedPercentage, DISPLAY_THRESHOLDS.DISK);
740
- const warning = diskSpace.usedPercentage >= DISPLAY_THRESHOLDS.DISK ? ' ⚠️' : '';
741
- message += `${usedBar} ${diskSpace.usedPercentage}%${warning}\n`;
742
+ const suffix = diskSpace.usedPercentage >= DISPLAY_THRESHOLDS.DISK ? ' ⚠️' : ' used';
743
+ message += `${usedBar} ${diskSpace.usedPercentage}%${suffix}\n`;
742
744
  message += `${formatBytesRange(diskSpace.usedBytes, diskSpace.totalBytes)}\n\n`;
743
745
  }
744
746
 
@@ -748,9 +750,9 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
748
750
  message += 'GitHub API\n';
749
751
  // Show used percentage with progress bar and threshold marker
750
752
  const usedBar = getProgressBar(githubRateLimit.usedPercentage, DISPLAY_THRESHOLDS.GITHUB_API);
751
- const warning = githubRateLimit.usedPercentage >= DISPLAY_THRESHOLDS.GITHUB_API ? ' ⚠️' : '';
752
- message += `${usedBar} ${githubRateLimit.usedPercentage}%${warning}\n`;
753
- message += `${githubRateLimit.used}/${githubRateLimit.limit} requests used\n`;
753
+ const suffix = githubRateLimit.usedPercentage >= DISPLAY_THRESHOLDS.GITHUB_API ? ' ⚠️' : ' used';
754
+ message += `${usedBar} ${githubRateLimit.usedPercentage}%${suffix}\n`;
755
+ message += `${githubRateLimit.used}/${githubRateLimit.limit} requests\n`;
754
756
  if (githubRateLimit.relativeReset) {
755
757
  message += `Resets in ${githubRateLimit.relativeReset} (${githubRateLimit.resetTime})\n`;
756
758
  } else if (githubRateLimit.resetTime) {
@@ -775,8 +777,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
775
777
  // See: https://github.com/link-assistant/hive-mind/issues/1133
776
778
  const pct = Math.floor(usage.currentSession.percentage);
777
779
  const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION);
778
- const warning = pct >= DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION ? ' ⚠️' : '';
779
- message += `${bar} ${pct}%${warning}\n`;
780
+ const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_5_HOUR_SESSION ? ' ⚠️' : ' used';
781
+ message += `${bar} ${pct}%${suffix}\n`;
780
782
 
781
783
  if (usage.currentSession.resetTime) {
782
784
  const relativeTime = formatRelativeTime(usage.currentSession.resetsAt);
@@ -807,8 +809,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
807
809
  // See: https://github.com/link-assistant/hive-mind/issues/1133
808
810
  const pct = Math.floor(usage.allModels.percentage);
809
811
  const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
810
- const warning = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : '';
811
- message += `${bar} ${pct}%${warning}\n`;
812
+ const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ' used';
813
+ message += `${bar} ${pct}%${suffix}\n`;
812
814
 
813
815
  if (usage.allModels.resetTime) {
814
816
  const relativeTime = formatRelativeTime(usage.allModels.resetsAt);
@@ -839,8 +841,8 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
839
841
  // See: https://github.com/link-assistant/hive-mind/issues/1133
840
842
  const pct = Math.floor(usage.sonnetOnly.percentage);
841
843
  const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY);
842
- const warning = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : '';
843
- message += `${bar} ${pct}%${warning}\n`;
844
+ const suffix = pct >= DISPLAY_THRESHOLDS.CLAUDE_WEEKLY ? ' ⚠️' : ' used';
845
+ message += `${bar} ${pct}%${suffix}\n`;
844
846
 
845
847
  if (usage.sonnetOnly.resetTime) {
846
848
  const relativeTime = formatRelativeTime(usage.sonnetOnly.resetsAt);
@@ -307,6 +307,7 @@ export const executeOpenCodeCommand = async params => {
307
307
  let limitReached = false;
308
308
  let limitResetTime = null;
309
309
  let lastMessage = '';
310
+ let lastTextContent = ''; // Issue #1263: Track last text content for result summary
310
311
  let allOutput = ''; // Collect all output for error detection
311
312
 
312
313
  for await (const chunk of execCommand.stream()) {
@@ -315,6 +316,41 @@ export const executeOpenCodeCommand = async params => {
315
316
  await log(output);
316
317
  lastMessage = output;
317
318
  allOutput += output;
319
+
320
+ // Issue #1263: Try to parse JSON output to extract text content for result summary
321
+ try {
322
+ const lines = output.split('\n');
323
+ for (const line of lines) {
324
+ if (!line.trim()) continue;
325
+ const data = JSON.parse(line);
326
+ // Track text content for result summary
327
+ // OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
328
+ if (data.type === 'text' && data.text) {
329
+ lastTextContent = data.text;
330
+ } else if (data.type === 'assistant' && data.message?.content) {
331
+ const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
332
+ for (const item of content) {
333
+ if (item.type === 'text' && item.text) {
334
+ lastTextContent = item.text;
335
+ }
336
+ }
337
+ } else if (data.type === 'message' && data.content) {
338
+ if (typeof data.content === 'string') {
339
+ lastTextContent = data.content;
340
+ } else if (Array.isArray(data.content)) {
341
+ for (const item of data.content) {
342
+ if (item.type === 'text' && item.text) {
343
+ lastTextContent = item.text;
344
+ }
345
+ }
346
+ }
347
+ } else if (data.type === 'result' && data.result) {
348
+ lastTextContent = data.result;
349
+ }
350
+ }
351
+ } catch {
352
+ // Not JSON, continue
353
+ }
318
354
  }
319
355
 
320
356
  if (chunk.type === 'stderr') {
@@ -322,6 +358,39 @@ export const executeOpenCodeCommand = async params => {
322
358
  if (errorOutput) {
323
359
  await log(errorOutput, { stream: 'stderr' });
324
360
  allOutput += errorOutput;
361
+
362
+ // Issue #1263: Also try to parse stderr for text content
363
+ try {
364
+ const lines = errorOutput.split('\n');
365
+ for (const line of lines) {
366
+ if (!line.trim()) continue;
367
+ const data = JSON.parse(line);
368
+ if (data.type === 'text' && data.text) {
369
+ lastTextContent = data.text;
370
+ } else if (data.type === 'assistant' && data.message?.content) {
371
+ const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
372
+ for (const item of content) {
373
+ if (item.type === 'text' && item.text) {
374
+ lastTextContent = item.text;
375
+ }
376
+ }
377
+ } else if (data.type === 'message' && data.content) {
378
+ if (typeof data.content === 'string') {
379
+ lastTextContent = data.content;
380
+ } else if (Array.isArray(data.content)) {
381
+ for (const item of data.content) {
382
+ if (item.type === 'text' && item.text) {
383
+ lastTextContent = item.text;
384
+ }
385
+ }
386
+ }
387
+ } else if (data.type === 'result' && data.result) {
388
+ lastTextContent = data.result;
389
+ }
390
+ }
391
+ } catch {
392
+ // Not JSON, continue
393
+ }
325
394
  }
326
395
  } else if (chunk.type === 'exit') {
327
396
  exitCode = chunk.code;
@@ -374,6 +443,7 @@ export const executeOpenCodeCommand = async params => {
374
443
  limitReached: false,
375
444
  limitResetTime: null,
376
445
  permissionPromptDetected: true,
446
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
377
447
  };
378
448
  }
379
449
 
@@ -409,16 +479,23 @@ export const executeOpenCodeCommand = async params => {
409
479
  sessionId,
410
480
  limitReached,
411
481
  limitResetTime,
482
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
412
483
  };
413
484
  }
414
485
 
415
486
  await log('\n\n✅ OpenCode command completed');
416
487
 
488
+ // Issue #1263: Log if result summary was captured
489
+ if (lastTextContent) {
490
+ await log('📝 Captured result summary from OpenCode output', { verbose: true });
491
+ }
492
+
417
493
  return {
418
494
  success: true,
419
495
  sessionId,
420
496
  limitReached,
421
497
  limitResetTime,
498
+ resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
422
499
  };
423
500
  } catch (error) {
424
501
  // Clean up the opencode.json config file even on error
@@ -441,6 +518,7 @@ export const executeOpenCodeCommand = async params => {
441
518
  sessionId: null,
442
519
  limitReached: false,
443
520
  limitResetTime: null,
521
+ resultSummary: null, // Issue #1263: No result summary available on error
444
522
  };
445
523
  }
446
524
  };
@@ -359,6 +359,16 @@ export const SOLVE_OPTION_DEFINITIONS = {
359
359
  description: 'Guide Claude to use agent-commander CLI (start-agent) instead of native Task tool for subagent delegation. Allows using any supported agent type (claude, opencode, codex, agent) with unified API. Only works with --tool claude and requires agent-commander to be installed.',
360
360
  default: false,
361
361
  },
362
+ 'attach-solution-summary': {
363
+ type: 'boolean',
364
+ description: 'Attach the AI solution summary (from the result field) as a comment to the PR/issue after completion. The summary is extracted from the AI tool JSON output and posted under a "Solution summary" header.',
365
+ default: false,
366
+ },
367
+ 'auto-attach-solution-summary': {
368
+ type: 'boolean',
369
+ description: 'Automatically attach solution summary only if the AI did not create any comments during the session. This provides visible feedback when the AI completes silently.',
370
+ default: false,
371
+ },
362
372
  };
363
373
 
364
374
  // Function to create yargs configuration - avoids duplication