@link-assistant/hive-mind 2.1.2 → 2.1.3
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 +6 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +132 -32
- package/src/bidirectional-interactive.lib.mjs +169 -71
- package/src/live-input-capabilities.lib.mjs +221 -0
- package/src/solve.auto-merge-helpers.lib.mjs +66 -0
- package/src/solve.auto-merge.lib.mjs +34 -5
- package/src/solve.config.lib.mjs +10 -10
- package/src/solve.validation.lib.mjs +3 -1
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Bidirectional Interactive Mode Library
|
|
4
4
|
*
|
|
5
|
-
* [EXPERIMENTAL] This module provides bidirectional real-time communication during
|
|
6
|
-
* It monitors PR comments for user feedback and queues
|
|
5
|
+
* [EXPERIMENTAL] This module provides bidirectional real-time communication during tool execution.
|
|
6
|
+
* It monitors issue/PR comments for user feedback and queues them for injection into the running tool session.
|
|
7
7
|
*
|
|
8
8
|
* Key features:
|
|
9
|
-
* - Monitors GitHub PR comments for new user feedback
|
|
10
|
-
* - Queues feedback messages for injection into
|
|
11
|
-
* - Works with Claude CLI
|
|
9
|
+
* - Monitors GitHub issue/PR comments for new user feedback
|
|
10
|
+
* - Queues feedback messages for injection into the tool stdin
|
|
11
|
+
* - Works with Claude/Agent CLI --input-format stream-json mode
|
|
12
12
|
* - Filters out system-generated comments (from interactive mode itself)
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
26
|
+
import { getLiveInputCapability, getLiveInputCapabilityRows, getLiveInputMode, isLiveInputSupported, LIVE_INPUT_MODE_FALLBACK, LIVE_INPUT_MODE_STREAM } from './live-input-capabilities.lib.mjs';
|
|
26
27
|
// Configuration constants
|
|
27
28
|
const CONFIG = {
|
|
28
29
|
// Minimum time between comment checks to avoid rate limiting (in ms)
|
|
@@ -31,9 +32,9 @@ const CONFIG = {
|
|
|
31
32
|
DEFAULT_POLL_INTERVAL: 15000,
|
|
32
33
|
// Maximum queued feedback messages
|
|
33
34
|
MAX_QUEUE_SIZE: 50,
|
|
34
|
-
// Default keep-alive for
|
|
35
|
-
//
|
|
36
|
-
// has replied, so new PR comments have a window to flow in as additional
|
|
35
|
+
// Default keep-alive for a headless stream-json process between turns.
|
|
36
|
+
// Claude Code exits after this many ms with no new input once it
|
|
37
|
+
// has replied, so new issue/PR comments have a window to flow in as additional
|
|
37
38
|
// user messages. Issue #817.
|
|
38
39
|
DEFAULT_EXIT_AFTER_STOP_DELAY_MS: 60_000,
|
|
39
40
|
// Signature to identify system-generated comments
|
|
@@ -54,12 +55,13 @@ const isSystemComment = body => {
|
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* Format a user feedback message for Claude
|
|
58
|
+
* Format a user feedback message for Claude-compatible stream-json input.
|
|
59
|
+
* Agent accepts the same `type: user` frame shape in stream-json mode.
|
|
58
60
|
*
|
|
59
61
|
* @param {string} feedbackText - The user's feedback text
|
|
60
62
|
* @param {Object} [options]
|
|
61
63
|
* @param {string} [options.kind='comment'] - Source kind: 'comment', 'ci', 'uncommitted', 'metadata'
|
|
62
|
-
* @returns {string} JSON string ready to write to
|
|
64
|
+
* @returns {string} JSON string ready to write to a stream-json stdin
|
|
63
65
|
*/
|
|
64
66
|
const formatFeedbackForClaude = (feedbackText, options = {}) => {
|
|
65
67
|
const kind = options.kind || 'comment';
|
|
@@ -98,10 +100,10 @@ const formatFeedbackForClaude = (feedbackText, options = {}) => {
|
|
|
98
100
|
};
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
|
-
* Build the first stream-json user frame for a
|
|
103
|
+
* Build the first stream-json user frame for a headless tool session.
|
|
102
104
|
*
|
|
103
105
|
* Issue #817: When --accept-incomming-comments-as-input is enabled, solve
|
|
104
|
-
* spawns
|
|
106
|
+
* spawns the tool with `--input-format stream-json` and a pipe stdin. The
|
|
105
107
|
* initial user prompt must therefore be delivered as a NDJSON frame rather
|
|
106
108
|
* than via `-p`. This matches the pattern from the reference gist
|
|
107
109
|
* `claude-stream-persistent.mjs`.
|
|
@@ -125,7 +127,7 @@ const buildInitialUserFrame = (promptText, options = {}) => {
|
|
|
125
127
|
};
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
|
-
* Write one NDJSON frame into a live
|
|
130
|
+
* Write one NDJSON frame into a live stream-json stdin stream.
|
|
129
131
|
*
|
|
130
132
|
* Returns true on a successful write, false when the stream is missing or
|
|
131
133
|
* closed. Never throws — callers just log and continue. Internal helper used
|
|
@@ -147,7 +149,7 @@ const writeFrameToStdin = async (stream, jsonFrame, logFn, verbose = false) => {
|
|
|
147
149
|
} catch (err) {
|
|
148
150
|
if (logFn && verbose) {
|
|
149
151
|
try {
|
|
150
|
-
await logFn(`⚠️ Bidirectional mode: Failed to write to
|
|
152
|
+
await logFn(`⚠️ Bidirectional mode: Failed to write to tool stdin: ${err.message}`, { verbose: true });
|
|
151
153
|
} catch {
|
|
152
154
|
/* ignore logger errors */
|
|
153
155
|
}
|
|
@@ -173,10 +175,11 @@ const writeFrameToStdin = async (stream, jsonFrame, logFn, verbose = false) => {
|
|
|
173
175
|
* @param {string} [options.deliveryMode='stream'] - 'stream' (immediate forward) or 'queue' (hold until AI idle). Issue #1708.
|
|
174
176
|
* @param {boolean} [options.streamStatusToInput=false] - Also stream CI/uncommitted/PR-status changes as NDJSON frames. Issue #1708.
|
|
175
177
|
* @param {number} [options.statusPollInterval=60000] - Status-poller interval (ms) when streamStatusToInput is on.
|
|
178
|
+
* @param {string} [options.toolLabel='AI tool'] - Human label for logging stdin writes.
|
|
176
179
|
* @returns {Object} Handler object with monitoring methods
|
|
177
180
|
*/
|
|
178
181
|
export const createBidirectionalHandler = options => {
|
|
179
|
-
const { owner, repo, prNumber, issueNumber, tempDir, $, log, verbose = false, pollInterval = CONFIG.DEFAULT_POLL_INTERVAL, excludeOwnComments = false, deliveryMode = 'stream', streamStatusToInput = false, statusPollInterval = 60000 } = options;
|
|
182
|
+
const { owner, repo, prNumber, issueNumber, tempDir, $, log, verbose = false, pollInterval = CONFIG.DEFAULT_POLL_INTERVAL, excludeOwnComments = false, deliveryMode = 'stream', streamStatusToInput = false, statusPollInterval = 60000, toolLabel = 'AI tool' } = options;
|
|
180
183
|
// Resolved lazily on first check, cached for the lifetime of the handler
|
|
181
184
|
let ownUserLogin = null;
|
|
182
185
|
let ownUserResolved = false;
|
|
@@ -205,7 +208,7 @@ export const createBidirectionalHandler = options => {
|
|
|
205
208
|
processedCommentIds: new Set(),
|
|
206
209
|
totalCommentsProcessed: 0,
|
|
207
210
|
totalFeedbackQueued: 0,
|
|
208
|
-
// Issue #817: Writable stdin of the live
|
|
211
|
+
// Issue #817/#2007: Writable stdin of the live stream-json process. When set, new
|
|
209
212
|
// non-system comments are written directly as NDJSON frames rather than
|
|
210
213
|
// only accumulated in feedbackQueue.
|
|
211
214
|
claudeStdin: null,
|
|
@@ -230,7 +233,36 @@ export const createBidirectionalHandler = options => {
|
|
|
230
233
|
};
|
|
231
234
|
|
|
232
235
|
/**
|
|
233
|
-
* Fetch
|
|
236
|
+
* Fetch comments from a GitHub API endpoint.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} apiPath
|
|
239
|
+
* @param {string} source
|
|
240
|
+
* @returns {Promise<Array>} Array of normalized comment objects
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
const fetchCommentsFromEndpoint = async (apiPath, source) => {
|
|
244
|
+
try {
|
|
245
|
+
const result = await $`gh api ${apiPath} --paginate --slurp`;
|
|
246
|
+
const parsed = JSON.parse(result.stdout?.toString() || '[]');
|
|
247
|
+
const comments = Array.isArray(parsed) && parsed.every(Array.isArray) ? parsed.flat() : parsed;
|
|
248
|
+
return comments.map(comment => ({
|
|
249
|
+
id: comment.id,
|
|
250
|
+
body: comment.body || '',
|
|
251
|
+
created_at: comment.created_at,
|
|
252
|
+
user: typeof comment.user === 'string' ? comment.user : comment.user?.login || '',
|
|
253
|
+
source,
|
|
254
|
+
}));
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (verbose) {
|
|
257
|
+
await log(`⚠️ Bidirectional mode: Failed to fetch ${source} comments: ${error.message}`, { verbose: true });
|
|
258
|
+
}
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fetch recent comments from PR conversation, PR review, and source issue.
|
|
265
|
+
*
|
|
234
266
|
* @returns {Promise<Array>} Array of comment objects
|
|
235
267
|
* @private
|
|
236
268
|
*/
|
|
@@ -242,24 +274,29 @@ export const createBidirectionalHandler = options => {
|
|
|
242
274
|
return [];
|
|
243
275
|
}
|
|
244
276
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return comments;
|
|
250
|
-
} catch (error) {
|
|
251
|
-
if (verbose) {
|
|
252
|
-
await log(`⚠️ Bidirectional mode: Failed to fetch comments: ${error.message}`, { verbose: true });
|
|
253
|
-
}
|
|
254
|
-
return [];
|
|
277
|
+
const comments = [...(await fetchCommentsFromEndpoint(`repos/${owner}/${repo}/issues/${prNumber}/comments`, 'pull request conversation')), ...(await fetchCommentsFromEndpoint(`repos/${owner}/${repo}/pulls/${prNumber}/comments`, 'pull request review'))];
|
|
278
|
+
|
|
279
|
+
if (issueNumber && String(issueNumber) !== String(prNumber)) {
|
|
280
|
+
comments.push(...(await fetchCommentsFromEndpoint(`repos/${owner}/${repo}/issues/${issueNumber}/comments`, 'issue')));
|
|
255
281
|
}
|
|
282
|
+
|
|
283
|
+
const seenKeys = new Set();
|
|
284
|
+
const uniqueComments = [];
|
|
285
|
+
for (const comment of comments) {
|
|
286
|
+
const key = comment.id == null ? `${comment.source}:${comment.created_at}:${comment.user}:${comment.body}` : String(comment.id);
|
|
287
|
+
if (seenKeys.has(key)) continue;
|
|
288
|
+
seenKeys.add(key);
|
|
289
|
+
uniqueComments.push(comment);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return uniqueComments.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
256
293
|
};
|
|
257
294
|
|
|
258
295
|
/**
|
|
259
296
|
* Issue #1708: Delivery-mode-aware frame dispatcher.
|
|
260
297
|
*
|
|
261
298
|
* - In stream mode (default for --accept-incomming-comments-as-input),
|
|
262
|
-
* the frame is written to
|
|
299
|
+
* the frame is written to the tool stdin immediately.
|
|
263
300
|
* - In queue mode (default for --auto-input-until-mergeable), the frame
|
|
264
301
|
* is buffered in state.pendingFrames while the AI is busy; on idle
|
|
265
302
|
* (markAiIdle) the buffer is flushed to stdin in FIFO order.
|
|
@@ -295,7 +332,7 @@ export const createBidirectionalHandler = options => {
|
|
|
295
332
|
if (ok) {
|
|
296
333
|
state.totalFeedbackStreamed++;
|
|
297
334
|
if (verbose) {
|
|
298
|
-
await log(`📤 Bidirectional mode: Streamed frame (${meta.kind || 'frame'}: ${meta.label || ''}) into
|
|
335
|
+
await log(`📤 Bidirectional mode: Streamed frame (${meta.kind || 'frame'}: ${meta.label || ''}) into ${toolLabel} stdin`, { verbose: true });
|
|
299
336
|
}
|
|
300
337
|
}
|
|
301
338
|
return ok;
|
|
@@ -339,7 +376,7 @@ export const createBidirectionalHandler = options => {
|
|
|
339
376
|
state.totalFramesFlushed++;
|
|
340
377
|
state.totalFeedbackStreamed++;
|
|
341
378
|
if (verbose) {
|
|
342
|
-
await log(`📤 Bidirectional mode: Flushed pending frame (${meta?.kind || 'frame'}: ${meta?.label || ''}) into
|
|
379
|
+
await log(`📤 Bidirectional mode: Flushed pending frame (${meta?.kind || 'frame'}: ${meta?.label || ''}) into ${toolLabel} stdin`, { verbose: true });
|
|
343
380
|
}
|
|
344
381
|
}
|
|
345
382
|
return flushed;
|
|
@@ -399,14 +436,14 @@ export const createBidirectionalHandler = options => {
|
|
|
399
436
|
state.totalFeedbackQueued++;
|
|
400
437
|
|
|
401
438
|
if (verbose) {
|
|
402
|
-
await log(`📥 Bidirectional mode: Queued feedback from @${comment.user} (comment #${comment.id})`, { verbose: true });
|
|
439
|
+
await log(`📥 Bidirectional mode: Queued feedback from @${comment.user} (${comment.source || 'comment'} #${comment.id})`, { verbose: true });
|
|
403
440
|
}
|
|
404
441
|
|
|
405
442
|
// Issue #817 / #1708: Dispatch through the delivery-mode router so
|
|
406
443
|
// queue-comments-to-input can hold the frame until the AI is idle.
|
|
407
444
|
await dispatchFrame(formattedMessage, {
|
|
408
445
|
kind: 'comment',
|
|
409
|
-
label:
|
|
446
|
+
label: `${comment.source || 'comment'} #${comment.id} from @${comment.user}`,
|
|
410
447
|
});
|
|
411
448
|
} else {
|
|
412
449
|
if (verbose) {
|
|
@@ -484,7 +521,7 @@ export const createBidirectionalHandler = options => {
|
|
|
484
521
|
* the same failing check doesn't re-emit on every poll.
|
|
485
522
|
*
|
|
486
523
|
* Failures in any sub-check are swallowed and logged — the poller must
|
|
487
|
-
* never break the live
|
|
524
|
+
* never break the live tool session.
|
|
488
525
|
*
|
|
489
526
|
* @private
|
|
490
527
|
*/
|
|
@@ -573,7 +610,7 @@ export const createBidirectionalHandler = options => {
|
|
|
573
610
|
};
|
|
574
611
|
|
|
575
612
|
/**
|
|
576
|
-
* Start monitoring PR comments for user feedback
|
|
613
|
+
* Start monitoring issue/PR comments for user feedback
|
|
577
614
|
*
|
|
578
615
|
* @returns {Promise<void>}
|
|
579
616
|
*/
|
|
@@ -604,7 +641,7 @@ export const createBidirectionalHandler = options => {
|
|
|
604
641
|
}, interval);
|
|
605
642
|
|
|
606
643
|
if (verbose) {
|
|
607
|
-
await log(`🔌 Bidirectional mode: Started monitoring PR #${prNumber} (polling every ${interval / 1000}s)`, { verbose: true });
|
|
644
|
+
await log(`🔌 Bidirectional mode: Started monitoring issue/PR comments for PR #${prNumber} (polling every ${interval / 1000}s)`, { verbose: true });
|
|
608
645
|
}
|
|
609
646
|
|
|
610
647
|
// Issue #1708: When --auto-input-until-mergeable enables status streaming,
|
|
@@ -632,7 +669,7 @@ export const createBidirectionalHandler = options => {
|
|
|
632
669
|
};
|
|
633
670
|
|
|
634
671
|
/**
|
|
635
|
-
* Stop monitoring PR comments
|
|
672
|
+
* Stop monitoring issue/PR comments
|
|
636
673
|
*
|
|
637
674
|
* @returns {Promise<void>}
|
|
638
675
|
*/
|
|
@@ -756,28 +793,32 @@ export const createBidirectionalHandler = options => {
|
|
|
756
793
|
};
|
|
757
794
|
|
|
758
795
|
/**
|
|
759
|
-
* Attach a live
|
|
796
|
+
* Attach a live tool stdin stream to the handler.
|
|
760
797
|
*
|
|
761
|
-
* Issue #817: Once attached, every new non-system comment detected by
|
|
762
|
-
* polling loop is also written to this stream as a NDJSON `user` frame.
|
|
798
|
+
* Issue #817/#2007: Once attached, every new non-system comment detected by
|
|
799
|
+
* the polling loop is also written to this stream as a NDJSON `user` frame.
|
|
763
800
|
* Safe to call before or after monitoring starts.
|
|
764
801
|
*
|
|
765
802
|
* @param {Object} stream - Writable stream (child.stdin)
|
|
766
803
|
*/
|
|
767
|
-
const
|
|
804
|
+
const attachToolStdin = stream => {
|
|
768
805
|
state.claudeStdin = stream || null;
|
|
769
806
|
};
|
|
770
807
|
|
|
771
808
|
/**
|
|
772
|
-
* Detach the
|
|
809
|
+
* Detach the tool stdin stream. After this call, comments are only queued.
|
|
773
810
|
*/
|
|
774
|
-
const
|
|
811
|
+
const detachToolStdin = () => {
|
|
775
812
|
state.claudeStdin = null;
|
|
776
813
|
};
|
|
777
814
|
|
|
815
|
+
// Compatibility aliases for the original Claude-only public surface.
|
|
816
|
+
const attachClaudeStdin = attachToolStdin;
|
|
817
|
+
const detachClaudeStdin = detachToolStdin;
|
|
818
|
+
|
|
778
819
|
/**
|
|
779
820
|
* Stream the initial user prompt as a stream-json frame into the attached
|
|
780
|
-
*
|
|
821
|
+
* tool stdin. Use this when running a tool with `--input-format stream-json`.
|
|
781
822
|
*
|
|
782
823
|
* @param {string} promptText
|
|
783
824
|
* @param {Object} [options]
|
|
@@ -791,7 +832,7 @@ export const createBidirectionalHandler = options => {
|
|
|
791
832
|
};
|
|
792
833
|
|
|
793
834
|
/**
|
|
794
|
-
* Stream a non-comment feedback message into the attached
|
|
835
|
+
* Stream a non-comment feedback message into the attached tool stdin.
|
|
795
836
|
*
|
|
796
837
|
* @param {string} feedbackText
|
|
797
838
|
* @param {Object} [options]
|
|
@@ -839,6 +880,8 @@ export const createBidirectionalHandler = options => {
|
|
|
839
880
|
markCommentAsProcessed,
|
|
840
881
|
initializeWithExistingComments,
|
|
841
882
|
initializeFromCurrentComments,
|
|
883
|
+
attachToolStdin,
|
|
884
|
+
detachToolStdin,
|
|
842
885
|
attachClaudeStdin,
|
|
843
886
|
detachClaudeStdin,
|
|
844
887
|
streamInitialPrompt,
|
|
@@ -870,8 +913,7 @@ export const createBidirectionalHandler = options => {
|
|
|
870
913
|
* @returns {boolean} Whether bidirectional interactive mode is supported
|
|
871
914
|
*/
|
|
872
915
|
export const isBidirectionalModeSupported = tool => {
|
|
873
|
-
|
|
874
|
-
return tool === 'claude';
|
|
916
|
+
return isLiveInputSupported(tool);
|
|
875
917
|
};
|
|
876
918
|
|
|
877
919
|
/**
|
|
@@ -891,18 +933,45 @@ export const isBidirectionalModeSupported = tool => {
|
|
|
891
933
|
* @returns {Promise<boolean>} Whether configuration is valid for the chosen tool
|
|
892
934
|
*/
|
|
893
935
|
export const validateBidirectionalModeConfig = async (argv, log) => {
|
|
894
|
-
// Issue #1708
|
|
895
|
-
// input-side of bidirectional mode
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
// streaming keeps the session alive.
|
|
936
|
+
// Issue #1708/#2007: --auto-input-until-mergeable enables only the
|
|
937
|
+
// input-side of bidirectional mode without enabling --interactive-mode
|
|
938
|
+
// (which would push tool output back as PR comments).
|
|
939
|
+
//
|
|
940
|
+
// Live event input is available for every tool, but the delivery mode differs:
|
|
941
|
+
// - stream-mode tools (Claude, Agent) get a live stdin pipe.
|
|
942
|
+
// - fallback-mode tools (Codex, opencode, gemini, qwen, ...) use the
|
|
943
|
+
// universal restart/resume fallback: the run finishes the current session
|
|
944
|
+
// in the JSON output, stops, and resumes the AI with the new issue/PR
|
|
945
|
+
// events as feedback via --auto-restart-until-mergeable.
|
|
905
946
|
if (argv.autoInputUntilMergeable) {
|
|
947
|
+
if (getLiveInputMode(argv.tool) === LIVE_INPUT_MODE_FALLBACK) {
|
|
948
|
+
// No live stdin channel for this tool: activate the restart/resume
|
|
949
|
+
// fallback instead of disabling the feature. Live comment streaming
|
|
950
|
+
// stays off; the auto-restart loop delivers the same events at session
|
|
951
|
+
// boundaries.
|
|
952
|
+
const capability = getLiveInputCapability(argv.tool);
|
|
953
|
+
argv.acceptIncommingCommentsAsInput = false;
|
|
954
|
+
argv.excludeAllOwnIncommingCommentsFromInput = false;
|
|
955
|
+
argv.streamCommentsToInput = false;
|
|
956
|
+
argv.queueCommentsToInput = false;
|
|
957
|
+
// Ensure the fallback loop is actually running. It defaults to enabled,
|
|
958
|
+
// but --auto-input-until-mergeable relies on it entirely for these tools,
|
|
959
|
+
// so re-enable it unless the user explicitly opted out.
|
|
960
|
+
if (argv.autoRestartUntilMergeable !== false) {
|
|
961
|
+
argv.autoRestartUntilMergeable = true;
|
|
962
|
+
}
|
|
963
|
+
await log(`🔁 --auto-input-until-mergeable: live streaming input is not available for --tool ${argv.tool}; using the restart/resume fallback.`, { level: 'info' });
|
|
964
|
+
await log(` ${capability.unsupportedReason}`, { level: 'info' });
|
|
965
|
+
if (capability.futureProtocol) {
|
|
966
|
+
await log(` Candidate live-streaming protocol: ${capability.futureProtocol} (tracked in link-assistant/agent).`, { level: 'info' });
|
|
967
|
+
}
|
|
968
|
+
if (argv.autoRestartUntilMergeable === false) {
|
|
969
|
+
await log(' ⚠️ --no-auto-restart-until-mergeable disables the fallback, so no live input mechanism remains for this tool.', { level: 'warning' });
|
|
970
|
+
} else {
|
|
971
|
+
await log(' The auto-restart-until-mergeable loop will resume the session with new issue/PR events (comments, title/body changes, CI failures, conflicts).', { level: 'info' });
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
906
975
|
if (!argv.acceptIncommingCommentsAsInput) argv.acceptIncommingCommentsAsInput = true;
|
|
907
976
|
// Default delivery mode for --auto-input-until-mergeable is queue:
|
|
908
977
|
// hold comments until the AI is idle so the model can finish the
|
|
@@ -935,10 +1004,24 @@ export const validateBidirectionalModeConfig = async (argv, log) => {
|
|
|
935
1004
|
// Nothing more to validate if no incoming-comment acceptance is requested
|
|
936
1005
|
if (!argv.acceptIncommingCommentsAsInput) return true;
|
|
937
1006
|
|
|
938
|
-
//
|
|
1007
|
+
// Live comment *streaming* is only wired for stream-mode tools (uses
|
|
1008
|
+
// --input-format stream-json). The universal restart/resume fallback is reached via
|
|
1009
|
+
// --auto-input-until-mergeable (handled above), not through the standalone
|
|
1010
|
+
// --accept-incomming-comments-as-input / --bidirectional-interactive-mode
|
|
1011
|
+
// flags, which are specifically about live streaming.
|
|
939
1012
|
if (!isBidirectionalModeSupported(argv.tool)) {
|
|
940
|
-
|
|
941
|
-
|
|
1013
|
+
const capability = getLiveInputCapability(argv.tool);
|
|
1014
|
+
const supportedTools = getLiveInputCapabilityRows()
|
|
1015
|
+
.filter(row => row.mode === LIVE_INPUT_MODE_STREAM)
|
|
1016
|
+
.map(row => `--tool ${row.tool}`)
|
|
1017
|
+
.join(' or ');
|
|
1018
|
+
await log(`⚠️ Live comment streaming is not supported for --tool ${argv.tool} in this build (supported: ${supportedTools}).`, { level: 'warning' });
|
|
1019
|
+
await log(` ${capability.unsupportedReason}`, { level: 'warning' });
|
|
1020
|
+
if (capability.futureProtocol) {
|
|
1021
|
+
await log(` Candidate follow-up protocol: ${capability.futureProtocol}.`, { level: 'warning' });
|
|
1022
|
+
}
|
|
1023
|
+
await log(' Live incoming-comment streaming will be disabled for this session.', { level: 'warning' });
|
|
1024
|
+
await log(' Tip: use --auto-input-until-mergeable to deliver the same issue/PR events through the restart/resume fallback instead.', { level: 'warning' });
|
|
942
1025
|
argv.acceptIncommingCommentsAsInput = false;
|
|
943
1026
|
argv.excludeAllOwnIncommingCommentsFromInput = false;
|
|
944
1027
|
argv.streamCommentsToInput = false;
|
|
@@ -947,10 +1030,11 @@ export const validateBidirectionalModeConfig = async (argv, log) => {
|
|
|
947
1030
|
}
|
|
948
1031
|
|
|
949
1032
|
const deliveryMode = argv.queueCommentsToInput ? 'queue' : 'stream';
|
|
1033
|
+
const capability = getLiveInputCapability(argv.tool);
|
|
950
1034
|
await log('🔌 Bidirectional Interactive Mode: ENABLED (experimental)', { level: 'info' });
|
|
951
1035
|
await log(` accept-incomming-comments-as-input: true${argv.excludeAllOwnIncommingCommentsFromInput ? ', exclude-all-own-incomming-comments-from-input: true' : ''}`, { level: 'info' });
|
|
952
1036
|
await log(` delivery mode: ${deliveryMode}-comments-to-input`, { level: 'info' });
|
|
953
|
-
await log(
|
|
1037
|
+
await log(` Issue/PR comments will be monitored and queued as feedback for ${capability.label}.`, { level: 'info' });
|
|
954
1038
|
|
|
955
1039
|
return true;
|
|
956
1040
|
};
|
|
@@ -979,6 +1063,8 @@ export const setupBidirectionalHandler = async ({ argv, owner, repo, prNumber, i
|
|
|
979
1063
|
await log('⚠️ Bidirectional mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
|
|
980
1064
|
return null;
|
|
981
1065
|
}
|
|
1066
|
+
const capability = getLiveInputCapability(argv.tool);
|
|
1067
|
+
const toolLabel = capability.label || argv.tool || 'AI tool';
|
|
982
1068
|
// Issue #1708: Resolve delivery mode from argv. validateBidirectionalModeConfig
|
|
983
1069
|
// already enforces queue-wins-over-stream and the per-flag defaults; here we
|
|
984
1070
|
// just translate the booleans into the handler-side enum.
|
|
@@ -988,7 +1074,7 @@ export const setupBidirectionalHandler = async ({ argv, owner, repo, prNumber, i
|
|
|
988
1074
|
// --accept-incomming-comments-as-input path keeps the existing #817 behavior
|
|
989
1075
|
// of forwarding only comments.
|
|
990
1076
|
const streamStatusToInput = !!argv.autoInputUntilMergeable;
|
|
991
|
-
await log(
|
|
1077
|
+
await log(`🔌 Bidirectional mode: Creating handler to accept incoming issue/PR comments as ${toolLabel} input`, { verbose: true });
|
|
992
1078
|
const handler = createBidirectionalHandler({
|
|
993
1079
|
owner,
|
|
994
1080
|
repo,
|
|
@@ -1002,6 +1088,7 @@ export const setupBidirectionalHandler = async ({ argv, owner, repo, prNumber, i
|
|
|
1002
1088
|
excludeOwnComments: !!argv.excludeAllOwnIncommingCommentsFromInput,
|
|
1003
1089
|
deliveryMode,
|
|
1004
1090
|
streamStatusToInput,
|
|
1091
|
+
toolLabel,
|
|
1005
1092
|
});
|
|
1006
1093
|
await handler.initializeFromCurrentComments();
|
|
1007
1094
|
await handler.startMonitoring();
|
|
@@ -1010,8 +1097,8 @@ export const setupBidirectionalHandler = async ({ argv, owner, repo, prNumber, i
|
|
|
1010
1097
|
};
|
|
1011
1098
|
|
|
1012
1099
|
/**
|
|
1013
|
-
* Attach a live
|
|
1014
|
-
*
|
|
1100
|
+
* Attach a live tool process to the handler so new comments stream into its
|
|
1101
|
+
* stdin as NDJSON frames. Also writes the initial user prompt as the
|
|
1015
1102
|
* first frame so the run starts normally. Issue #817.
|
|
1016
1103
|
*
|
|
1017
1104
|
* Safe to call with a null handler (no-op). Logs diagnostics but never throws.
|
|
@@ -1021,19 +1108,26 @@ export const setupBidirectionalHandler = async ({ argv, owner, repo, prNumber, i
|
|
|
1021
1108
|
* @param {string} prompt - Initial user prompt text
|
|
1022
1109
|
* @param {Function} log
|
|
1023
1110
|
* @param {boolean} [verbose=false]
|
|
1111
|
+
* @param {Object} [options]
|
|
1112
|
+
* @param {string} [options.toolLabel='AI tool']
|
|
1024
1113
|
* @returns {Promise<boolean>} Whether streaming input is active
|
|
1025
1114
|
*/
|
|
1026
|
-
export const attachStreamingInput = async (handler, execCommand, prompt, log, verbose = false) => {
|
|
1115
|
+
export const attachStreamingInput = async (handler, execCommand, prompt, log, verbose = false, options = {}) => {
|
|
1027
1116
|
if (!handler || !execCommand) return false;
|
|
1117
|
+
const toolLabel = options.toolLabel || 'AI tool';
|
|
1028
1118
|
try {
|
|
1029
1119
|
const stdinStream = await execCommand.streams.stdin;
|
|
1030
1120
|
if (!stdinStream) {
|
|
1031
|
-
if (verbose) await log(
|
|
1121
|
+
if (verbose) await log(`⚠️ Bidirectional mode: Could not acquire ${toolLabel} stdin stream; falling back to queued-only feedback.`, { verbose: true });
|
|
1032
1122
|
return false;
|
|
1033
1123
|
}
|
|
1034
|
-
handler.
|
|
1124
|
+
if (typeof handler.attachToolStdin === 'function') {
|
|
1125
|
+
handler.attachToolStdin(stdinStream);
|
|
1126
|
+
} else {
|
|
1127
|
+
handler.attachClaudeStdin(stdinStream);
|
|
1128
|
+
}
|
|
1035
1129
|
const ok = await handler.streamInitialPrompt(prompt);
|
|
1036
|
-
if (verbose) await log(`🔌 Bidirectional mode: Streaming input ${ok ? 'ENABLED' : 'FAILED'} (wrote initial user frame to
|
|
1130
|
+
if (verbose) await log(`🔌 Bidirectional mode: Streaming input ${ok ? 'ENABLED' : 'FAILED'} (wrote initial user frame to ${toolLabel} stdin).`, { verbose: true });
|
|
1037
1131
|
return ok;
|
|
1038
1132
|
} catch (attachError) {
|
|
1039
1133
|
await log(`⚠️ Bidirectional mode: Failed to attach stdin (${attachError.message}); continuing without live streaming.`, { verbose: true });
|
|
@@ -1052,7 +1146,11 @@ export const attachStreamingInput = async (handler, execCommand, prompt, log, ve
|
|
|
1052
1146
|
export const finalizeBidirectionalHandler = async (handler, log) => {
|
|
1053
1147
|
if (!handler) return [];
|
|
1054
1148
|
try {
|
|
1055
|
-
handler.
|
|
1149
|
+
if (typeof handler.detachToolStdin === 'function') {
|
|
1150
|
+
handler.detachToolStdin();
|
|
1151
|
+
} else {
|
|
1152
|
+
handler.detachClaudeStdin?.();
|
|
1153
|
+
}
|
|
1056
1154
|
await handler.stopMonitoring();
|
|
1057
1155
|
const state = handler.getState();
|
|
1058
1156
|
const queuedFeedback = handler.getAllQueuedFeedback();
|
|
@@ -1062,14 +1160,14 @@ export const finalizeBidirectionalHandler = async (handler, log) => {
|
|
|
1062
1160
|
await log(` • From @${feedback.user}: ${feedback.body.substring(0, 100)}${feedback.body.length > 100 ? '...' : ''}`, { level: 'info' });
|
|
1063
1161
|
}
|
|
1064
1162
|
if (state.totalFeedbackStreamed > 0) {
|
|
1065
|
-
await log(` 📤 ${state.totalFeedbackStreamed} of these were streamed live into
|
|
1163
|
+
await log(` 📤 ${state.totalFeedbackStreamed} of these were streamed live into the tool stdin.`, { level: 'info' });
|
|
1066
1164
|
} else {
|
|
1067
1165
|
await log(' 💡 This feedback will be available for the next continuation of this task.', { level: 'info' });
|
|
1068
1166
|
}
|
|
1069
1167
|
} else {
|
|
1070
1168
|
await log('📊 Bidirectional mode: No new feedback received during execution', { verbose: true });
|
|
1071
1169
|
}
|
|
1072
|
-
await log(`📊 Bidirectional mode stats: ${state.totalCommentsProcessed} comments processed, ${state.totalFeedbackQueued} feedback queued, ${state.totalFeedbackStreamed} streamed into
|
|
1170
|
+
await log(`📊 Bidirectional mode stats: ${state.totalCommentsProcessed} comments processed, ${state.totalFeedbackQueued} feedback queued, ${state.totalFeedbackStreamed} streamed into tool stdin`, { verbose: true });
|
|
1073
1171
|
return queuedFeedback;
|
|
1074
1172
|
} catch (bidirectionalError) {
|
|
1075
1173
|
await log(`⚠️ Bidirectional mode cleanup error: ${bidirectionalError.message}`, { verbose: true });
|