@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 +31 -0
- package/package.json +2 -2
- package/src/agent.lib.mjs +59 -0
- package/src/claude.lib.mjs +21 -20
- package/src/codex.lib.mjs +34 -0
- package/src/github-merge.lib.mjs +27 -3
- package/src/limits.lib.mjs +18 -16
- 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/src/telegram-bot.mjs +17 -10
- package/src/telegram-merge-command.lib.mjs +59 -8
- package/src/telegram-merge-queue.lib.mjs +18 -0
- package/src/telegram-solve-queue-command.lib.mjs +5 -7
- package/src/telegram-solve-queue.lib.mjs +147 -55
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.
|
|
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
|
};
|
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/github-merge.lib.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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') {
|
package/src/limits.lib.mjs
CHANGED
|
@@ -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
|
-
|
|
718
|
-
|
|
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
|
|
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
|
|
730
|
-
message += `${usedBar} ${memory.usedPercentage}%${
|
|
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
|
|
741
|
-
message += `${usedBar} ${diskSpace.usedPercentage}%${
|
|
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
|
|
752
|
-
message += `${usedBar} ${githubRateLimit.usedPercentage}%${
|
|
753
|
-
message += `${githubRateLimit.used}/${githubRateLimit.limit} requests
|
|
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
|
|
779
|
-
message += `${bar} ${pct}%${
|
|
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
|
|
811
|
-
message += `${bar} ${pct}%${
|
|
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
|
|
843
|
-
message += `${bar} ${pct}%${
|
|
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);
|
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
|