@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 +14 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +59 -0
- package/src/claude.lib.mjs +21 -20
- package/src/codex.lib.mjs +34 -0
- package/src/opencode.lib.mjs +78 -0
- package/src/solve.config.lib.mjs +10 -0
- package/src/solve.mjs +37 -1
- package/src/solve.results.lib.mjs +130 -0
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
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
|
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
};
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -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
|
};
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -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
|
+
};
|