@link-assistant/hive-mind 1.21.4 → 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,19 @@
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
+
3
17
  ## 1.21.4
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.21.4",
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",
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
  };
@@ -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
package/src/solve.mjs CHANGED
@@ -58,7 +58,7 @@ const { processAutoContinueForIssue } = autoContinue;
58
58
  const repository = await import('./solve.repository.lib.mjs');
59
59
  const { setupTempDirectory, cleanupTempDirectory } = repository;
60
60
  const results = await import('./solve.results.lib.mjs');
61
- const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand } = results;
61
+ const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, checkForAiCreatedComments, attachSolutionSummary } = results;
62
62
  const claudeLib = await import('./claude.lib.mjs');
63
63
  const { executeClaude } = claudeLib;
64
64
 
@@ -910,6 +910,7 @@ try {
910
910
  let publicPricingEstimate = toolResult.publicPricingEstimate; // Used by agent tool
911
911
  let pricingInfo = toolResult.pricingInfo; // Used by agent tool for detailed pricing
912
912
  let errorDuringExecution = toolResult.errorDuringExecution || false; // Issue #1088: Track error_during_execution
913
+ let resultSummary = toolResult.resultSummary || null; // Issue #1263: Capture result summary for --attach-solution-summary
913
914
  limitReached = toolResult.limitReached;
914
915
  cleanupContext.limitReached = limitReached;
915
916
 
@@ -1183,6 +1184,41 @@ try {
1183
1184
  // Show summary of session and log file
1184
1185
  await showSessionSummary(sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs);
1185
1186
 
1187
+ // Issue #1263: Handle solution summary attachment
1188
+ // --attach-solution-summary: Always attach if result summary is available
1189
+ // --auto-attach-solution-summary: Only attach if AI didn't create any comments during session
1190
+ if (success && resultSummary && (argv.attachSolutionSummary || argv.autoAttachSolutionSummary)) {
1191
+ let shouldAttachSummary = false;
1192
+
1193
+ if (argv.attachSolutionSummary) {
1194
+ // Explicit flag - always attach
1195
+ shouldAttachSummary = true;
1196
+ await log('📝 --attach-solution-summary enabled, attaching result summary...');
1197
+ } else if (argv.autoAttachSolutionSummary) {
1198
+ // Auto mode - only attach if AI didn't create comments
1199
+ await log('🔍 Checking if AI created any comments during session (--auto-attach-solution-summary)...');
1200
+ const aiCreatedComments = await checkForAiCreatedComments(referenceTime, owner, repo, prNumber, issueNumber);
1201
+ if (aiCreatedComments) {
1202
+ await log('ℹ️ AI created comments during session, skipping solution summary attachment');
1203
+ } else {
1204
+ shouldAttachSummary = true;
1205
+ await log('📝 No AI comments detected, attaching solution summary...');
1206
+ }
1207
+ }
1208
+
1209
+ if (shouldAttachSummary) {
1210
+ await attachSolutionSummary({
1211
+ resultSummary,
1212
+ prNumber,
1213
+ issueNumber,
1214
+ owner,
1215
+ repo,
1216
+ });
1217
+ }
1218
+ } else if ((argv.attachSolutionSummary || argv.autoAttachSolutionSummary) && !resultSummary) {
1219
+ await log('ℹ️ No solution summary available from AI tool output', { verbose: true });
1220
+ }
1221
+
1186
1222
  // Search for newly created pull requests and comments
1187
1223
  // Pass shouldRestart to prevent early exit when auto-restart is needed
1188
1224
  // Include agent tool pricing data when available (publicPricingEstimate, pricingInfo)
@@ -876,3 +876,133 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
876
876
 
877
877
  await safeExit(1, 'Execution error');
878
878
  };
879
+
880
+ /**
881
+ * Check if new comments were created by the AI during the session.
882
+ * This is used by --auto-attach-solution-summary to determine if the AI
883
+ * already provided feedback.
884
+ *
885
+ * Issue #1263: Support for --attach-solution-summary and --auto-attach-solution-summary
886
+ *
887
+ * @param {Date} referenceTime - The timestamp before tool execution
888
+ * @param {string} owner - Repository owner
889
+ * @param {string} repo - Repository name
890
+ * @param {number} prNumber - Pull request number (null if working on issue only)
891
+ * @param {number} issueNumber - Issue number
892
+ * @returns {Promise<boolean>} - True if AI created comments during the session
893
+ */
894
+ export const checkForAiCreatedComments = async (referenceTime, owner, repo, prNumber, issueNumber) => {
895
+ try {
896
+ // Get the current user's GitHub username
897
+ const userResult = await $`gh api user --jq .login`;
898
+ if (userResult.code !== 0) {
899
+ return false; // Cannot determine, default to not attaching
900
+ }
901
+ const currentUser = userResult.stdout.toString().trim();
902
+ if (!currentUser) {
903
+ return false;
904
+ }
905
+
906
+ // Check comments on the PR first (if we have a PR)
907
+ if (prNumber) {
908
+ // Check PR conversation comments
909
+ const prCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${prNumber}/comments --paginate`;
910
+ if (prCommentsResult.code === 0) {
911
+ const prComments = JSON.parse(prCommentsResult.stdout.toString().trim() || '[]');
912
+ const newPrComments = prComments.filter(comment => comment.user.login === currentUser && new Date(comment.created_at) > referenceTime);
913
+ if (newPrComments.length > 0) {
914
+ return true;
915
+ }
916
+ }
917
+
918
+ // Check PR review comments (inline code comments)
919
+ const reviewCommentsResult = await $`gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate`;
920
+ if (reviewCommentsResult.code === 0) {
921
+ const reviewComments = JSON.parse(reviewCommentsResult.stdout.toString().trim() || '[]');
922
+ const newReviewComments = reviewComments.filter(comment => comment.user.login === currentUser && new Date(comment.created_at) > referenceTime);
923
+ if (newReviewComments.length > 0) {
924
+ return true;
925
+ }
926
+ }
927
+ }
928
+
929
+ // Check issue comments (if different from PR number or no PR)
930
+ if (issueNumber && issueNumber !== prNumber) {
931
+ const issueCommentsResult = await $`gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --paginate`;
932
+ if (issueCommentsResult.code === 0) {
933
+ const issueComments = JSON.parse(issueCommentsResult.stdout.toString().trim() || '[]');
934
+ const newIssueComments = issueComments.filter(comment => comment.user.login === currentUser && new Date(comment.created_at) > referenceTime);
935
+ if (newIssueComments.length > 0) {
936
+ return true;
937
+ }
938
+ }
939
+ }
940
+
941
+ return false;
942
+ } catch (error) {
943
+ // On error, default to not attaching (safer choice)
944
+ await log(`⚠️ Could not check for AI comments: ${error.message}`, { verbose: true });
945
+ return false;
946
+ }
947
+ };
948
+
949
+ /**
950
+ * Attach the AI's solution summary as a comment to the PR or issue.
951
+ * The summary is extracted from the tool's result field and posted
952
+ * with a "Solution summary" header.
953
+ *
954
+ * Issue #1263: Support for --attach-solution-summary and --auto-attach-solution-summary
955
+ *
956
+ * @param {Object} options - Options object
957
+ * @param {string} options.resultSummary - The AI's result summary text
958
+ * @param {number} options.prNumber - Pull request number (null if posting to issue)
959
+ * @param {number} options.issueNumber - Issue number
960
+ * @param {string} options.owner - Repository owner
961
+ * @param {string} options.repo - Repository name
962
+ * @returns {Promise<boolean>} - True if comment was posted successfully
963
+ */
964
+ export const attachSolutionSummary = async ({ resultSummary, prNumber, issueNumber, owner, repo }) => {
965
+ if (!resultSummary || typeof resultSummary !== 'string') {
966
+ await log('⚠️ No solution summary available to attach', { verbose: true });
967
+ return false;
968
+ }
969
+
970
+ const targetNumber = prNumber || issueNumber;
971
+ const targetType = prNumber ? 'pr' : 'issue';
972
+ const ghCommand = prNumber ? 'pr' : 'issue';
973
+
974
+ if (!targetNumber) {
975
+ await log('⚠️ No PR or issue number to attach solution summary to', { verbose: true });
976
+ return false;
977
+ }
978
+
979
+ try {
980
+ const comment = `## Solution summary
981
+
982
+ ${resultSummary}
983
+
984
+ ---
985
+ *This summary was automatically extracted from the AI working session output.*`;
986
+
987
+ const result = await $`gh ${ghCommand} comment ${targetNumber} --repo ${owner}/${repo} --body ${comment}`;
988
+
989
+ if (result.code === 0) {
990
+ await log(`✅ Solution summary attached to ${targetType} #${targetNumber}`);
991
+ return true;
992
+ } else {
993
+ await log(`⚠️ Failed to attach solution summary: ${result.stderr?.toString() || 'Unknown error'}`, {
994
+ level: 'warning',
995
+ });
996
+ return false;
997
+ }
998
+ } catch (error) {
999
+ reportError(error, {
1000
+ context: 'attach_solution_summary',
1001
+ targetType,
1002
+ targetNumber,
1003
+ operation: 'post_solution_summary_comment',
1004
+ });
1005
+ await log(`⚠️ Error attaching solution summary: ${error.message}`, { level: 'warning' });
1006
+ return false;
1007
+ }
1008
+ };