@link-assistant/hive-mind 1.47.1 → 1.47.2

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.47.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 7afe67b: Fix ghPrView false positive on "Could not resolve" in PR body causing "Failed to get PR details" error on fork PRs, and add stdio log interceptor for terminal/log output parity
8
+
3
9
  ## 1.47.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.47.1",
3
+ "version": "1.47.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -1328,7 +1328,7 @@ export async function ghPrView({ prNumber, owner, repo, jsonFields = 'headRefNam
1328
1328
  const stderr = prResult.stderr ? prResult.stderr.toString() : '';
1329
1329
  const code = prResult.code || 0;
1330
1330
  let data = null;
1331
- if (code === 0 && stdout && !stdout.includes('Could not resolve')) {
1331
+ if (code === 0 && stdout && !(stderr && stderr.includes('Could not resolve'))) {
1332
1332
  try {
1333
1333
  data = JSON.parse(stdout);
1334
1334
  } catch {
@@ -1368,7 +1368,7 @@ export async function ghIssueView({ issueNumber, owner, repo, jsonFields = 'numb
1368
1368
  const stderr = issueResult.stderr ? issueResult.stderr.toString() : '';
1369
1369
  const code = issueResult.code || 0;
1370
1370
  let data = null;
1371
- if (code === 0 && stdout && !stdout.includes('Could not resolve')) {
1371
+ if (code === 0 && stdout && !(stderr && stderr.includes('Could not resolve'))) {
1372
1372
  try {
1373
1373
  data = JSON.parse(stdout);
1374
1374
  } catch {
package/src/hive.mjs CHANGED
@@ -95,7 +95,7 @@ if (isDirectExecution) {
95
95
  const fs = (await withTimeout(use('fs'), 30000, 'loading fs')).promises;
96
96
  // Import shared library functions
97
97
  const lib = await import('./lib.mjs');
98
- const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories, setupVerboseLogInterceptor } = lib;
98
+ const { log, setLogFile, getAbsoluteLogPath, formatTimestamp, cleanErrorMessage, cleanupTempDirectories, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
99
99
  const yargsConfigLib = await import('./hive.config.lib.mjs');
100
100
  const { createYargsConfig } = yargsConfigLib;
101
101
  const claudeLib = await import('./claude.lib.mjs');
@@ -309,8 +309,8 @@ if (isDirectExecution) {
309
309
  // Set global verbose mode
310
310
  global.verboseMode = argv.verbose;
311
311
 
312
- // Issue #1466: Intercept console.log to capture [VERBOSE] output in log files
313
- setupVerboseLogInterceptor();
312
+ setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
313
+ setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
314
314
 
315
315
  // Use the universal GitHub URL parser
316
316
  if (githubUrl) {
package/src/lib.mjs CHANGED
@@ -99,18 +99,24 @@ export const log = async (message, options = {}) => {
99
99
  }
100
100
 
101
101
  // Write to console based on level
102
- switch (level) {
103
- case 'error':
104
- console.error(message);
105
- break;
106
- case 'warning':
107
- case 'warn':
108
- console.warn(message);
109
- break;
110
- case 'info':
111
- default:
112
- console.log(message);
113
- break;
102
+ // Set guard flag to prevent stdio interceptor from double-logging (issue #1549)
103
+ _writingFromLog = true;
104
+ try {
105
+ switch (level) {
106
+ case 'error':
107
+ console.error(message);
108
+ break;
109
+ case 'warning':
110
+ case 'warn':
111
+ console.warn(message);
112
+ break;
113
+ case 'info':
114
+ default:
115
+ console.log(message);
116
+ break;
117
+ }
118
+ } finally {
119
+ _writingFromLog = false;
114
120
  }
115
121
  };
116
122
 
@@ -134,20 +140,90 @@ export const setupVerboseLogInterceptor = () => {
134
140
 
135
141
  const originalConsoleLog = console.log.bind(console);
136
142
  console.log = (...args) => {
137
- // Always call original console.log first
138
- originalConsoleLog(...args);
139
-
140
143
  // If a log file is set and the message looks like a [VERBOSE] log, append to file
144
+ // and set guard flag to prevent stdio interceptor from double-logging (issue #1549)
141
145
  if (logFile && args.length > 0) {
142
146
  const firstArg = String(args[0]);
143
147
  if (firstArg.includes('[VERBOSE]')) {
144
148
  const message = args.map(a => String(a)).join(' ');
145
149
  const logMessage = `[${new Date().toISOString()}] [VERBOSE] ${message}`;
150
+ _writingFromLog = true;
151
+ fs.appendFile(logFile, logMessage + '\n').catch(() => {
152
+ // Silent fail to avoid infinite loops
153
+ });
154
+ }
155
+ }
156
+
157
+ // Always call original console.log (with guard flag set if [VERBOSE])
158
+ try {
159
+ originalConsoleLog(...args);
160
+ } finally {
161
+ _writingFromLog = false;
162
+ }
163
+ };
164
+ };
165
+
166
+ /**
167
+ * Issue #1549: Intercept process.stdout.write and process.stderr.write to capture
168
+ * ALL terminal output in the log file, ensuring 100% parity between terminal and log.
169
+ *
170
+ * The command-stream library uses process.stdout.write/process.stderr.write directly
171
+ * when mirror:true (the default). console.log/console.error also end up calling these.
172
+ * By intercepting at the write() level, we capture everything regardless of source:
173
+ * - command-stream mirror output (e.g., gh CLI JSON responses)
174
+ * - console.log() / console.error() calls
175
+ * - process.stdout.write() / process.stderr.write() direct calls
176
+ *
177
+ * To avoid double-logging (since the log() function already writes to the log file AND
178
+ * calls console.log which calls process.stdout.write), we use a guard flag
179
+ * `_writingFromLog` to skip interception when the write originates from log().
180
+ *
181
+ * This ensures the log file is a complete record of all terminal output.
182
+ * Call this once after setLogFile() to enable the interceptor.
183
+ */
184
+ let stdioInterceptorInstalled = false;
185
+ let _writingFromLog = false; // Guard flag to prevent double-logging from log()
186
+ export const setupStdioLogInterceptor = () => {
187
+ if (stdioInterceptorInstalled) return;
188
+ stdioInterceptorInstalled = true;
189
+
190
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
191
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
192
+
193
+ process.stdout.write = (chunk, encoding, callback) => {
194
+ // Always write to terminal first
195
+ const result = originalStdoutWrite(chunk, encoding, callback);
196
+
197
+ // Also append to log file if set, but skip if this write originated from log()
198
+ if (logFile && !_writingFromLog) {
199
+ const text = typeof chunk === 'string' ? chunk : chunk.toString(encoding || 'utf8');
200
+ if (text.trim()) {
201
+ const logMessage = `[${new Date().toISOString()}] [STDOUT] ${text.replace(/\n$/, '')}`;
202
+ fs.appendFile(logFile, logMessage + '\n').catch(() => {
203
+ // Silent fail to avoid infinite loops
204
+ });
205
+ }
206
+ }
207
+
208
+ return result;
209
+ };
210
+
211
+ process.stderr.write = (chunk, encoding, callback) => {
212
+ // Always write to terminal first
213
+ const result = originalStderrWrite(chunk, encoding, callback);
214
+
215
+ // Also append to log file if set, but skip if this write originated from log()
216
+ if (logFile && !_writingFromLog) {
217
+ const text = typeof chunk === 'string' ? chunk : chunk.toString(encoding || 'utf8');
218
+ if (text.trim()) {
219
+ const logMessage = `[${new Date().toISOString()}] [STDERR] ${text.replace(/\n$/, '')}`;
146
220
  fs.appendFile(logFile, logMessage + '\n').catch(() => {
147
221
  // Silent fail to avoid infinite loops
148
222
  });
149
223
  }
150
224
  }
225
+
226
+ return result;
151
227
  };
152
228
  };
153
229
 
@@ -611,6 +687,7 @@ export default {
611
687
  displayFormattedError,
612
688
  cleanupTempDirectories,
613
689
  setupVerboseLogInterceptor,
690
+ setupStdioLogInterceptor,
614
691
  };
615
692
 
616
693
  /**
package/src/solve.mjs CHANGED
@@ -48,7 +48,7 @@ const fs = (await use('fs')).promises;
48
48
  const crypto = (await use('crypto')).default;
49
49
  const memoryCheck = await import('./memory-check.mjs');
50
50
  const lib = await import('./lib.mjs');
51
- const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor } = lib;
51
+ const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, getVersionInfo, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
52
52
  const githubLib = await import('./github.lib.mjs');
53
53
  const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
54
54
  const validation = await import('./solve.validation.lib.mjs');
@@ -111,8 +111,8 @@ try {
111
111
  }
112
112
  global.verboseMode = argv.verbose;
113
113
 
114
- // Issue #1466: Intercept console.log to capture [VERBOSE] output in log files
115
- setupVerboseLogInterceptor();
114
+ setupVerboseLogInterceptor(); // Issue #1466: capture [VERBOSE] output in log files
115
+ setupStdioLogInterceptor(); // Issue #1549: capture ALL terminal output in log file
116
116
 
117
117
  // Early logs go to cwd; custom log dir takes effect after argv is parsed
118
118