@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.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Shared capability matrix for live issue/PR event input.
3
+ *
4
+ * Issue #2007 asks solve to feed issue/PR events into the running AI tool "in
5
+ * all ways possible", and to provide a universal fallback for every tool that
6
+ * does not have a mid-session live input channel: wait for the current turn to
7
+ * finish in the JSON output, stop the process, and resume the AI session with
8
+ * the new events.
9
+ *
10
+ * Because of that fallback, live event input is *available* for every tool.
11
+ * Tools differ only in the delivery `mode`:
12
+ *
13
+ * - `stream` : the tool exposes a live stdin/JSON channel, so new events are
14
+ * written into the running process without restarting it. Claude
15
+ * and Agent (`--input-format stream-json`) are wired for this today.
16
+ * - `fallback`: no verified mid-session input channel exists yet, so solve uses
17
+ * the restart/resume loop (`--auto-restart-until-mergeable` /
18
+ * `watchUntilMergeable`). It waits for the current session to end,
19
+ * then resumes/restarts the AI with the new issue/PR events as
20
+ * feedback. This works for every tool.
21
+ *
22
+ * Missing native live-input features for each tool are reported upstream in the
23
+ * https://github.com/link-assistant/agent repository so they can be implemented
24
+ * (see `agentIssue`), after which a tool can graduate from `fallback` to `stream`.
25
+ */
26
+
27
+ export const ISSUE_2007_REQUIRED_EVENT_IDS = Object.freeze(['issue-title', 'issue-body', 'issue-comments', 'pull-request-comments']);
28
+
29
+ export const LIVE_INPUT_EVENT_SOURCES = Object.freeze([
30
+ Object.freeze({
31
+ id: 'issue-title',
32
+ label: 'Issue title updates',
33
+ requiredByIssue2007: true,
34
+ note: 'User-facing issue metadata should be streamed when it changes during a run.',
35
+ }),
36
+ Object.freeze({
37
+ id: 'issue-body',
38
+ label: 'Issue description updates',
39
+ requiredByIssue2007: true,
40
+ note: 'The issue body is treated as user feedback because it can change after the agent starts.',
41
+ }),
42
+ Object.freeze({
43
+ id: 'issue-comments',
44
+ label: 'Issue comments',
45
+ requiredByIssue2007: true,
46
+ note: 'New non-system issue comments are user feedback.',
47
+ }),
48
+ Object.freeze({
49
+ id: 'pull-request-comments',
50
+ label: 'Pull request comments',
51
+ requiredByIssue2007: true,
52
+ note: 'New non-system PR conversation comments and review comments should reach the agent.',
53
+ }),
54
+ Object.freeze({
55
+ id: 'pull-request-description',
56
+ label: 'Pull request description updates',
57
+ requiredByIssue2007: false,
58
+ note: 'Issue #2007 explicitly treats the PR description as AI-owned, so it is not a required user-feedback source.',
59
+ }),
60
+ ]);
61
+
62
+ const REQUIRED_EVENTS = ISSUE_2007_REQUIRED_EVENT_IDS;
63
+
64
+ // Delivery modes for live issue/PR event input.
65
+ export const LIVE_INPUT_MODE_STREAM = 'stream';
66
+ export const LIVE_INPUT_MODE_FALLBACK = 'fallback';
67
+
68
+ // Shared description of the universal restart/resume fallback so every tool
69
+ // entry reports it identically.
70
+ const FALLBACK_DESCRIPTION = 'Universal fallback: wait for the current AI turn to finish in the JSON output, stop the process, then resume/restart the AI session with the new issue/PR events as feedback via --auto-restart-until-mergeable (watchUntilMergeable). Works for every tool even without a live stdin channel.';
71
+
72
+ const CAPABILITIES = Object.freeze({
73
+ claude: Object.freeze({
74
+ tool: 'claude',
75
+ label: 'Claude',
76
+ available: true,
77
+ mode: LIVE_INPUT_MODE_STREAM,
78
+ liveStreaming: true,
79
+ supported: true,
80
+ option: '--auto-input-until-mergeable',
81
+ protocol: 'claude --input-format stream-json stdin NDJSON',
82
+ currentRunner: 'src/claude.lib.mjs keeps stdin as a pipe and attaches bidirectional-interactive.lib.mjs',
83
+ futureProtocol: '',
84
+ fallback: FALLBACK_DESCRIPTION,
85
+ events: REQUIRED_EVENTS,
86
+ agentIssue: '',
87
+ unsupportedReason: '',
88
+ testing: 'Run solve with --tool claude --auto-input-until-mergeable, add an issue or PR comment while the Claude process is alive, and watch for the bidirectional handler to queue or stream a user frame into stdin.',
89
+ }),
90
+ codex: Object.freeze({
91
+ tool: 'codex',
92
+ label: 'Codex',
93
+ available: true,
94
+ mode: LIVE_INPUT_MODE_FALLBACK,
95
+ liveStreaming: false,
96
+ supported: false,
97
+ option: '--auto-input-until-mergeable',
98
+ protocol: 'Restart/resume fallback (no live stdin wired through solve for Codex yet).',
99
+ currentRunner: 'src/codex.lib.mjs uses codex exec with prompt/stdin context at process start',
100
+ futureProtocol: 'Codex app-server JSON-RPC turn/steer',
101
+ fallback: FALLBACK_DESCRIPTION,
102
+ events: REQUIRED_EVENTS,
103
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
104
+ unsupportedReason: 'The current solve Codex runner uses codex exec, whose stdin is one-shot prompt/context at process start. It does not expose a live JSON input pipe for mid-session issue/PR events, so the restart/resume fallback is used. Codex app-server turn/steer is the candidate protocol for a future live-streaming Codex runner.',
105
+ testing: 'Passing --tool codex --auto-input-until-mergeable activates the restart/resume fallback: the run finishes the current session, then resumes with the new issue/PR events.',
106
+ }),
107
+ agent: Object.freeze({
108
+ tool: 'agent',
109
+ label: 'Agent',
110
+ available: true,
111
+ mode: LIVE_INPUT_MODE_STREAM,
112
+ liveStreaming: true,
113
+ supported: true,
114
+ option: '--auto-input-until-mergeable',
115
+ protocol: 'agent --input-format stream-json --output-format stream-json stdin/stdout NDJSON',
116
+ currentRunner: 'src/agent.lib.mjs keeps stdin as a pipe and attaches bidirectional-interactive.lib.mjs when live input is enabled',
117
+ futureProtocol: '',
118
+ fallback: FALLBACK_DESCRIPTION,
119
+ events: REQUIRED_EVENTS,
120
+ agentIssue: 'https://github.com/link-assistant/agent/pull/274',
121
+ unsupportedReason: '',
122
+ testing: 'Run solve with --tool agent --auto-input-until-mergeable, add an issue or PR comment while the Agent process is alive, and watch for the bidirectional handler to queue or stream a user frame into stdin.',
123
+ }),
124
+ opencode: Object.freeze({
125
+ tool: 'opencode',
126
+ label: 'OpenCode',
127
+ available: true,
128
+ mode: LIVE_INPUT_MODE_FALLBACK,
129
+ liveStreaming: false,
130
+ supported: false,
131
+ option: '--auto-input-until-mergeable',
132
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for OpenCode yet).',
133
+ currentRunner: 'src/opencode.lib.mjs uses a prompt-via-file/stdin pattern',
134
+ futureProtocol: '',
135
+ fallback: FALLBACK_DESCRIPTION,
136
+ events: REQUIRED_EVENTS,
137
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
138
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for OpenCode yet, so the restart/resume fallback is used.',
139
+ testing: 'Passing --tool opencode --auto-input-until-mergeable activates the restart/resume fallback.',
140
+ }),
141
+ gemini: Object.freeze({
142
+ tool: 'gemini',
143
+ label: 'Gemini',
144
+ available: true,
145
+ mode: LIVE_INPUT_MODE_FALLBACK,
146
+ liveStreaming: false,
147
+ supported: false,
148
+ option: '--auto-input-until-mergeable',
149
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for Gemini yet).',
150
+ currentRunner: 'src/gemini.lib.mjs uses a prompt-driven process invocation',
151
+ futureProtocol: '',
152
+ fallback: FALLBACK_DESCRIPTION,
153
+ events: REQUIRED_EVENTS,
154
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
155
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for Gemini yet, so the restart/resume fallback is used.',
156
+ testing: 'Passing --tool gemini --auto-input-until-mergeable activates the restart/resume fallback.',
157
+ }),
158
+ qwen: Object.freeze({
159
+ tool: 'qwen',
160
+ label: 'Qwen',
161
+ available: true,
162
+ mode: LIVE_INPUT_MODE_FALLBACK,
163
+ liveStreaming: false,
164
+ supported: false,
165
+ option: '--auto-input-until-mergeable',
166
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for Qwen yet).',
167
+ currentRunner: 'src/qwen.lib.mjs uses a prompt-driven process invocation',
168
+ futureProtocol: '',
169
+ fallback: FALLBACK_DESCRIPTION,
170
+ events: REQUIRED_EVENTS,
171
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
172
+ unsupportedReason: 'No verified live JSON input channel is wired through solve for Qwen yet, so the restart/resume fallback is used.',
173
+ testing: 'Passing --tool qwen --auto-input-until-mergeable activates the restart/resume fallback.',
174
+ }),
175
+ });
176
+
177
+ const UNKNOWN_CAPABILITY = tool =>
178
+ Object.freeze({
179
+ tool,
180
+ label: tool,
181
+ available: true,
182
+ mode: LIVE_INPUT_MODE_FALLBACK,
183
+ liveStreaming: false,
184
+ supported: false,
185
+ option: '--auto-input-until-mergeable',
186
+ protocol: 'Restart/resume fallback (no live JSON input channel wired through solve for this tool yet).',
187
+ currentRunner: 'Unknown or custom solve tool runner',
188
+ futureProtocol: '',
189
+ fallback: FALLBACK_DESCRIPTION,
190
+ events: REQUIRED_EVENTS,
191
+ agentIssue: 'https://github.com/link-assistant/agent/issues',
192
+ unsupportedReason: `No verified live JSON input channel is wired through solve for ${tool}, so the restart/resume fallback is used. Add a live-input capability entry (and report the missing native API to link-assistant/agent) once the runner has a long-lived stdin, JSON-RPC, or SDK channel that accepts new user turns mid-session.`,
193
+ testing: 'The flag activates the restart/resume fallback until a live-streaming runner is implemented.',
194
+ });
195
+
196
+ export const getLiveInputCapability = tool => {
197
+ const normalizedTool = String(tool || '')
198
+ .trim()
199
+ .toLowerCase();
200
+ return CAPABILITIES[normalizedTool] || UNKNOWN_CAPABILITY(normalizedTool || 'unknown');
201
+ };
202
+
203
+ /**
204
+ * Whether the tool has a live *streaming* input channel (writes events into the
205
+ * running process). Kept as `isLiveInputSupported` for backward compatibility;
206
+ * it is the stream-mode predicate, not "is live input available at all".
207
+ */
208
+ export const isLiveInputSupported = tool => getLiveInputCapability(tool).mode === LIVE_INPUT_MODE_STREAM;
209
+
210
+ /**
211
+ * Whether live issue/PR event input is available for the tool in *any* mode
212
+ * (streaming or restart/resume fallback). This is true for every tool.
213
+ */
214
+ export const isLiveInputAvailable = tool => getLiveInputCapability(tool).available === true;
215
+
216
+ /**
217
+ * Resolve the delivery mode ('stream' or 'fallback') for a tool.
218
+ */
219
+ export const getLiveInputMode = tool => getLiveInputCapability(tool).mode;
220
+
221
+ export const getLiveInputCapabilityRows = () => Object.values(CAPABILITIES);
@@ -956,9 +956,75 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
956
956
  return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false, noWorkflowRunsForCommit };
957
957
  };
958
958
 
959
+ /**
960
+ * Issue #2007: Detect issue title/description changes across auto-restart
961
+ * iterations so the restart/resume fallback can deliver them as feedback for
962
+ * tools without a live input channel.
963
+ *
964
+ * The issue title and body are user-owned feedback surfaces (unlike the PR
965
+ * description, which #2007 treats as AI-owned). When they change while the AI
966
+ * is not streaming input, the next session must be told, otherwise the update
967
+ * would be silently ignored until the agent happens to re-read the issue.
968
+ *
969
+ * The first call (previousSnapshot = null) establishes the baseline and reports
970
+ * no change. Subsequent calls diff against the prior snapshot.
971
+ *
972
+ * @param {string} owner - Repository owner
973
+ * @param {string} repo - Repository name
974
+ * @param {number} issueNumber - Linked issue number
975
+ * @param {Object|null} previousSnapshot - Prior { title, body } snapshot, or null on first call
976
+ * @param {boolean} [verbose=false]
977
+ * @param {Function} [commandRunner=$] - Tagged-template command runner (injectable for tests)
978
+ * @returns {Promise<{changed: boolean, snapshot: Object|null, changes: Array<{field: string, from: string, to: string}>}>}
979
+ */
980
+ export const checkForIssueMetadataChanges = async (owner, repo, issueNumber, previousSnapshot, verbose = false, commandRunner = $) => {
981
+ const empty = { changed: false, snapshot: previousSnapshot || null, changes: [] };
982
+ if (!issueNumber) return empty;
983
+
984
+ let snapshot;
985
+ try {
986
+ const result = await commandRunner`gh api repos/${owner}/${repo}/issues/${issueNumber} --jq '{title: .title, body: .body}'`;
987
+ if (result.code !== 0 || !result.stdout) return empty;
988
+ const parsed = JSON.parse(result.stdout.toString() || '{}');
989
+ snapshot = {
990
+ title: typeof parsed.title === 'string' ? parsed.title : '',
991
+ body: typeof parsed.body === 'string' ? parsed.body : '',
992
+ };
993
+ } catch (error) {
994
+ reportError(error, {
995
+ context: 'check_issue_metadata_changes',
996
+ owner,
997
+ repo,
998
+ issueNumber,
999
+ operation: 'fetch_issue_metadata',
1000
+ });
1001
+ return empty;
1002
+ }
1003
+
1004
+ // First observation: establish the baseline without reporting a change.
1005
+ if (!previousSnapshot) {
1006
+ return { changed: false, snapshot, changes: [] };
1007
+ }
1008
+
1009
+ const changes = [];
1010
+ if (snapshot.title !== previousSnapshot.title) {
1011
+ changes.push({ field: 'title', from: previousSnapshot.title, to: snapshot.title });
1012
+ }
1013
+ if (snapshot.body !== previousSnapshot.body) {
1014
+ changes.push({ field: 'body', from: previousSnapshot.body, to: snapshot.body });
1015
+ }
1016
+
1017
+ if (verbose && changes.length > 0) {
1018
+ console.log(`[VERBOSE] Issue #${issueNumber} metadata changed: ${changes.map(c => c.field).join(', ')}`);
1019
+ }
1020
+
1021
+ return { changed: changes.length > 0, snapshot, changes };
1022
+ };
1023
+
959
1024
  export default {
960
1025
  checkForExistingComment,
961
1026
  checkForNonBotComments,
1027
+ checkForIssueMetadataChanges,
962
1028
  getMergeBlockers,
963
1029
  shouldResetNoRunsCounter,
964
1030
  };
@@ -59,7 +59,7 @@ import { limitReset } from './config.lib.mjs';
59
59
 
60
60
  // Import helper functions extracted for file size management (Issue #1593)
61
61
  const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
62
- const { checkForExistingComment, checkForNonBotComments, getMergeBlockers, shouldResetNoRunsCounter, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
62
+ const { checkForExistingComment, checkForNonBotComments, checkForIssueMetadataChanges, getMergeBlockers, shouldResetNoRunsCounter, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
63
63
 
64
64
  // Issue #1769: cancelled/stale CI re-run failures need a human action stop, not polling forever.
65
65
  const cancelledCiRerunLib = await import('./cancelled-ci-rerun.lib.mjs');
@@ -137,7 +137,7 @@ export const watchUntilMergeable = async params => {
137
137
  await log(formatAligned('', 'Max limit resumes:', formatAutoIterationLimit(maxAutoResumeIterations), 2));
138
138
  await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (strict repo-wide safety)' : 'No (PR-scoped CI only)', 2));
139
139
  await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
140
- await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
140
+ await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, issue title/description edits, CI failures, merge conflicts', 2));
141
141
  // Issue #1708: Surface that --auto-input-until-mergeable streamed feedback
142
142
  // into the prior session, so any restart triggered here is a fallback.
143
143
  if (argv.autoInputUntilMergeable) {
@@ -157,6 +157,11 @@ export const watchUntilMergeable = async params => {
157
157
  let iteration = 0;
158
158
  let lastCheckTime = new Date();
159
159
 
160
+ // Issue #2007: Track the issue title/body across iterations so the
161
+ // restart/resume fallback can detect user edits to those surfaces and deliver
162
+ // them as feedback to the next session. The first check seeds the baseline.
163
+ let issueMetadataSnapshot = null;
164
+
160
165
  while (true) {
161
166
  iteration++;
162
167
  const currentTime = new Date();
@@ -250,6 +255,14 @@ export const watchUntilMergeable = async params => {
250
255
  trustAuthenticatedUserComments: true,
251
256
  });
252
257
 
258
+ // Issue #2007: Detect issue title/description edits (user-owned feedback
259
+ // surfaces) so the fallback resumes the AI with them. The first iteration
260
+ // seeds the baseline and never reports a change.
261
+ const metadataCheck = await checkForIssueMetadataChanges(owner, repo, issueNumber, issueMetadataSnapshot, argv.verbose, $);
262
+ issueMetadataSnapshot = metadataCheck.snapshot || issueMetadataSnapshot;
263
+ const hasIssueMetadataChanges = metadataCheck.changed === true;
264
+ const issueMetadataChanges = metadataCheck.changes || [];
265
+
253
266
  // Check for uncommitted changes using shared utility
254
267
  const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
255
268
 
@@ -264,8 +277,9 @@ export const watchUntilMergeable = async params => {
264
277
  }
265
278
  }
266
279
 
267
- // If PR is mergeable, no blockers, no new comments, and no uncommitted changes
268
- if (blockers.length === 0 && !hasNewComments && !hasUncommittedChanges) {
280
+ // If PR is mergeable, no blockers, no new comments, no issue metadata
281
+ // edits, and no uncommitted changes
282
+ if (blockers.length === 0 && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges) {
269
283
  // Issue #1503 (enhanced): Multi-mechanism consensus + repo-wide action check.
270
284
  // Before declaring PR mergeable, run multiple independent CI detection mechanisms
271
285
  // and require all to agree. This catches race conditions where CI starts between
@@ -420,6 +434,21 @@ export const watchUntilMergeable = async params => {
420
434
  feedbackLines.push('Please review and address the feedback from these comments.');
421
435
  }
422
436
 
437
+ // Issue #2007: Reason 1b: Issue title/description edited by the user.
438
+ if (hasIssueMetadataChanges) {
439
+ shouldRestart = true;
440
+ const changedFields = issueMetadataChanges.map(c => (c.field === 'title' ? 'title' : 'description')).join(' and ');
441
+ restartReason = restartReason ? `${restartReason}; Issue ${changedFields} edited` : `Issue ${changedFields} edited`;
442
+ feedbackLines.push(`✏️ The issue ${changedFields} was edited after the last session:`);
443
+ for (const change of issueMetadataChanges) {
444
+ const label = change.field === 'title' ? 'Issue title' : 'Issue description';
445
+ const updated = String(change.to ?? '');
446
+ feedbackLines.push(` - ${label} is now: "${updated.substring(0, 200)}${updated.length > 200 ? '...' : ''}"`);
447
+ }
448
+ feedbackLines.push('');
449
+ feedbackLines.push('Please re-read the updated issue and make sure your solution still matches the requirements.');
450
+ }
451
+
423
452
  // Issue #1314: Check for billing limit errors BEFORE regular CI failures
424
453
  // Billing limits require human intervention and should NOT trigger AI restarts
425
454
  const billingBlocker = blockers.find(b => b.type === 'billing_limit');
@@ -581,7 +610,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
581
610
  // take the restart path rather than the cancelled-review path.
582
611
  const ciBlocker = ciFailureBlocker;
583
612
  const hasMergeConflictBlocker = blockers.some(b => b.type === 'not_mergeable' && b.message?.includes('conflicts'));
584
- if (externalReviewLimitBlocker && !ciBlocker && !billingBlocker && !cancelledBlocker && !hasNewComments && !hasUncommittedChanges && !hasMergeConflictBlocker) {
613
+ if (externalReviewLimitBlocker && !ciBlocker && !billingBlocker && !cancelledBlocker && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges && !hasMergeConflictBlocker) {
585
614
  await log('');
586
615
  await log(formatAligned('🟡', 'READY FOR REVIEW', 'External review quota/credit limit requires human decision'));
587
616
  for (const detail of externalReviewLimitBlocker.details || []) {
@@ -242,13 +242,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
242
242
  description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
243
243
  default: true,
244
244
  },
245
- // Issue #1708: Stage 1 introduces this flag inert — it parses, appears in
246
- // --help, and is read by validateAutoInputUntilMergeable below, but does not
247
- // change the runtime loop yet. Stages 2-6 will wire it into watchUntilMergeable
248
- // and the bidirectional NDJSON pipe (see docs/case-studies/issue-1708/).
245
+ // Issue #1708/#2007: streaming-first feedback into the running tool session.
246
+ // Claude and Agent are wired through bidirectional stream-json stdin pipes;
247
+ // other tools retain restart/resume fallback behavior until a verified
248
+ // mid-session input protocol is wired into their solve runners.
249
249
  'auto-input-until-mergeable': {
250
250
  type: 'boolean',
251
- description: '[EXPERIMENTAL] Extend a single AI tool session as long as possible by streaming new input (uncommitted changes, CI/CD failures, PR/issue comments, issue title/body updates) directly into the running session, instead of restarting it. Implies --accept-incomming-comments-as-input and --queue-comments-to-input by default (comments are deferred until the AI finishes the current step and is waiting for input). Existing auto-restart/auto-resume loops remain enabled as a fallback, but the goal is to keep them dormant. The full streaming-aware watchUntilMergeable replacement and per-tool wiring is staged in subsequent PRs (see docs/case-studies/issue-1708/). Falls back gracefully on non-Claude tools and on streaming errors. Disabled by default.',
251
+ description: '[EXPERIMENTAL] Keep feeding new issue/PR events (uncommitted changes, CI/CD failures, PR/issue comments, issue title/description edits) into the running AI session, in all ways possible. For --tool claude and --tool agent this streams the events directly into the live process via stream-json stdin (implies --accept-incomming-comments-as-input and --queue-comments-to-input by default, deferring comments until the AI finishes the current step). For codex, opencode, gemini, qwen, and unknown tools, it uses the universal restart/resume fallback: wait for the current turn to finish in the JSON output, stop the process, then resume/restart the AI session with the new events via --auto-restart-until-mergeable. Codex live streaming should be wired in a future runner through Codex app-server turn/steer. Disabled by default.',
252
252
  default: false,
253
253
  },
254
254
  'wait-for-all-actions-in-repository-before-mergeable': {
@@ -436,7 +436,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
436
436
  // Issue #817: Bidirectional interactive options
437
437
  'accept-incomming-comments-as-input': {
438
438
  type: 'boolean',
439
- description: '[EXPERIMENTAL] Accept new PR/issue comments as input for Claude during execution (excludes outgoing comments generated by solve itself). Does not require --interactive-mode; disabled by default. Only supported for --tool claude.',
439
+ description: '[EXPERIMENTAL] Accept new PR/issue comments as input for the running stream-json tool during execution (excludes outgoing comments generated by solve itself). Does not require --interactive-mode; disabled by default. Only supported for --tool claude and --tool agent.',
440
440
  default: false,
441
441
  },
442
442
  'exclude-all-own-incomming-comments-from-input': {
@@ -446,13 +446,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
446
446
  },
447
447
  'bidirectional-interactive-mode': {
448
448
  type: 'boolean',
449
- description: '[EXPERIMENTAL] Convenience flag that enables --interactive-mode, --accept-incomming-comments-as-input and --exclude-all-own-incomming-comments-from-input together. Only supported for --tool claude.',
449
+ description: '[EXPERIMENTAL] Convenience flag that enables --interactive-mode, --accept-incomming-comments-as-input and --exclude-all-own-incomming-comments-from-input together. Only supported for --tool claude and --tool agent.',
450
450
  default: false,
451
451
  },
452
452
  // Issue #1708: Comment delivery mode for --accept-incomming-comments-as-input.
453
453
  // --stream-comments-to-input: forward comments immediately as they arrive
454
454
  // (the default for --accept-incomming-comments-as-input on its own; matches
455
- // the existing #817 behavior of pushing comments to Claude as soon as
455
+ // the existing #817 behavior of pushing comments to the stream-json tool as soon as
456
456
  // pollIncomingComments sees them).
457
457
  // --queue-comments-to-input: hold comments until the AI signals it is idle
458
458
  // (waiting for input), then flush the queue. Used by
@@ -461,12 +461,12 @@ export const SOLVE_OPTION_DEFINITIONS = {
461
461
  // The two flags are mutually exclusive; if both are set, queue mode wins.
462
462
  'stream-comments-to-input': {
463
463
  type: 'boolean',
464
- description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, forward each new PR/issue comment to the AI immediately as it arrives (real-time streaming). This is the default behavior for --accept-incomming-comments-as-input on its own. Mutually exclusive with --queue-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
464
+ description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, forward each new PR/issue comment to the AI immediately as it arrives (real-time streaming). This is the default behavior for --accept-incomming-comments-as-input on its own. Mutually exclusive with --queue-comments-to-input; queue mode wins if both are set. Only supported for --tool claude and --tool agent.',
465
465
  default: false,
466
466
  },
467
467
  'queue-comments-to-input': {
468
468
  type: 'boolean',
469
- description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, queue new PR/issue comments and only flush them once the AI signals it is idle (waiting for input). This is the default mode implied by --auto-input-until-mergeable so the AI completes the current step before being interrupted with new instructions. Mutually exclusive with --stream-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
469
+ description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, queue new PR/issue comments and only flush them once the AI signals it is idle (waiting for input). This is the default mode implied by --auto-input-until-mergeable so the AI completes the current step before being interrupted with new instructions. Mutually exclusive with --stream-comments-to-input; queue mode wins if both are set. Only supported for --tool claude and --tool agent.',
470
470
  default: false,
471
471
  },
472
472
  'prompt-explore-sub-agent': {
@@ -341,7 +341,9 @@ export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnecti
341
341
  } else if (argv.tool === 'agent') {
342
342
  // Validate Agent connection
343
343
  const agentLib = await import('./agent.lib.mjs');
344
- isToolConnected = await agentLib.validateAgentConnection(model);
344
+ isToolConnected = await agentLib.validateAgentConnection(model, {
345
+ requireLiveInput: !!(argv.autoInputUntilMergeable || argv.acceptIncommingCommentsAsInput),
346
+ });
345
347
  if (!isToolConnected) {
346
348
  await log('❌ Cannot proceed without Agent connection', { level: 'error' });
347
349
  return false;