@link-assistant/hive-mind 2.0.26 → 2.0.28

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.
@@ -355,7 +355,7 @@ en
355
355
  queue
356
356
  only
357
357
  in
358
- groups "❌ The /solve_queue command only works in group chats. Please add this bot to a group and make it an admin."
358
+ groups "❌ The /queue command only works in group chats. Please add this bot to a group and make it an admin."
359
359
  disabled "❌ The solve command is disabled on this bot instance."
360
360
  rejected """
361
361
  ❌ Solve command rejected.
@@ -413,7 +413,7 @@ en
413
413
  URL: {{url}}
414
414
  Status: {{status}}
415
415
 
416
- 💡 Use /solve_queue to check the queue status.
416
+ 💡 Use /queue to check the queue status.
417
417
  """
418
418
  session
419
419
  running """
@@ -422,7 +422,7 @@ en
422
422
  URL: {{url}}
423
423
  Session: `{{session}}`
424
424
 
425
- 💡 Wait for the current session to complete, or use /solve\_stop to cancel it.
425
+ 💡 Wait for the current session to complete, or use /stop to cancel it.
426
426
  """
427
427
  issues
428
428
  list
@@ -515,7 +515,7 @@ en
515
515
  detail "Tool aliases imply `--tool <tool>`: `/codex <github-url>` equals `/solve <github-url> --tool codex`"
516
516
  reply "Or reply to a message with a GitHub link: `/solve`"
517
517
  disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Disabled"
518
- queue "`/solve_queue` (alias: `/queue`) - Show solve queue status"
518
+ queue "`/queue` - Show solve queue status"
519
519
  locked
520
520
  options "🔒 Locked options: `{{options}}`"
521
521
  task
@@ -553,7 +553,7 @@ en
553
553
  isolation
554
554
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
555
555
  group
556
- note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
556
+ note "⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /watch, /subscribe and /unsubscribe work in private and group chats."
557
557
  common
558
558
  options "🔧 *Common Options:*"
559
559
  model
@@ -355,7 +355,7 @@ hi
355
355
  queue
356
356
  only
357
357
  in
358
- groups "❌ /solve_queue कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
358
+ groups "❌ /queue कमांड केवल समूह चैट में काम करती है। कृपया बॉट को समूह में जोड़ें और व्यवस्थापक बनाएँ।"
359
359
  disabled "❌ इस बॉट इंस्टेंस पर solve कमांड अक्षम है।"
360
360
  rejected """
361
361
  ❌ Solve कमांड अस्वीकृत।
@@ -413,7 +413,7 @@ hi
413
413
  URL: {{url}}
414
414
  स्थिति: {{status}}
415
415
 
416
- 💡 कतार स्थिति देखने के लिए /solve_queue का उपयोग करें।
416
+ 💡 कतार स्थिति देखने के लिए /queue का उपयोग करें।
417
417
  """
418
418
  session
419
419
  running """
@@ -422,7 +422,7 @@ hi
422
422
  URL: {{url}}
423
423
  सत्र: `{{session}}`
424
424
 
425
- 💡 वर्तमान सत्र के पूरा होने की प्रतीक्षा करें, या रद्द करने के लिए /solve\_stop का उपयोग करें।
425
+ 💡 वर्तमान सत्र के पूरा होने की प्रतीक्षा करें, या रद्द करने के लिए /stop का उपयोग करें।
426
426
  """
427
427
  issues
428
428
  list
@@ -515,7 +515,7 @@ hi
515
515
  detail "Tool aliases `--tool <tool>` लगाते हैं: `/codex <github-url>` का अर्थ `/solve <github-url> --tool codex` है"
516
516
  reply "या GitHub लिंक वाले संदेश का उत्तर दें: `/solve`"
517
517
  disabled "*/solve* (aliases: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ अक्षम"
518
- queue "`/solve_queue` (उपनाम: `/queue`) - solve queue status दिखाएँ"
518
+ queue "`/queue` - solve queue status दिखाएँ"
519
519
  locked
520
520
  options "🔒 लॉक किए गए विकल्प: `{{options}}`"
521
521
  task
@@ -553,7 +553,7 @@ hi
553
553
  isolation
554
554
  mode "🔒 *Isolation Mode:* `{{isolationBackend}}` (experimental)"
555
555
  group
556
- note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
556
+ note "⚠️ *नोट:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop और /start commands केवल group chats में काम करती हैं। /terminal\\_watch, /watch, /subscribe और /unsubscribe private और group chats में काम करती हैं।"
557
557
  common
558
558
  options "🔧 *Common Options:*"
559
559
  model
@@ -355,7 +355,7 @@ ru
355
355
  queue
356
356
  only
357
357
  in
358
- groups "❌ Команда /solve_queue работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
358
+ groups "❌ Команда /queue работает только в групповых чатах. Добавьте бота в группу и сделайте его администратором."
359
359
  disabled "❌ Команда solve отключена в этом экземпляре бота."
360
360
  rejected """
361
361
  ❌ Команда solve отклонена.
@@ -413,7 +413,7 @@ ru
413
413
  URL: {{url}}
414
414
  Статус: {{status}}
415
415
 
416
- 💡 Используйте /solve_queue для проверки очереди.
416
+ 💡 Используйте /queue для проверки очереди.
417
417
  """
418
418
  session
419
419
  running """
@@ -422,7 +422,7 @@ ru
422
422
  URL: {{url}}
423
423
  Сеанс: `{{session}}`
424
424
 
425
- 💡 Дождитесь завершения текущего сеанса или используйте /solve\_stop для отмены.
425
+ 💡 Дождитесь завершения текущего сеанса или используйте /stop для отмены.
426
426
  """
427
427
  issues
428
428
  list
@@ -515,7 +515,7 @@ ru
515
515
  detail "Алиасы инструментов добавляют `--tool <tool>`: `/codex <github-url>` равно `/solve <github-url> --tool codex`"
516
516
  reply "Или ответьте на сообщение со ссылкой GitHub: `/solve`"
517
517
  disabled "*/solve* (алиасы: */do*, */continue*, */claude*, */codex*, */opencode*, */agent*, */gemini*, */qwen*) - ❌ Отключено"
518
- queue "`/solve_queue` (псевдоним: `/queue`) - Показать состояние очереди solve"
518
+ queue "`/queue` - Показать состояние очереди solve"
519
519
  locked
520
520
  options "🔒 Заблокированные опции: `{{options}}`"
521
521
  task
@@ -553,7 +553,7 @@ ru
553
553
  isolation
554
554
  mode "🔒 *Режим изоляции:* `{{isolationBackend}}` (экспериментально)"
555
555
  group
556
- note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
556
+ note "⚠️ *Замечание:* команды /solve, /do, /continue, /claude, /codex, /opencode, /agent, /gemini, /qwen, /task, /split, /hive, /queue, /limits, /version, /accept\\_invites, /merge, /stop и /start работают только в групповых чатах. /terminal\\_watch, /watch, /subscribe и /unsubscribe работают в личных и групповых чатах."
557
557
  common
558
558
  options "🔧 *Общие опции:*"
559
559
  model
@@ -355,7 +355,7 @@ zh
355
355
  queue
356
356
  only
357
357
  in
358
- groups "❌ /solve_queue 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
358
+ groups "❌ /queue 命令仅在群聊中有效。请将本机器人加入群组并设为管理员。"
359
359
  disabled "❌ 此机器人实例已禁用 solve 命令。"
360
360
  rejected """
361
361
  ❌ Solve 命令被拒绝。
@@ -413,7 +413,7 @@ zh
413
413
  URL:{{url}}
414
414
  状态:{{status}}
415
415
 
416
- 💡 使用 /solve_queue 查看队列状态。
416
+ 💡 使用 /queue 查看队列状态。
417
417
  """
418
418
  session
419
419
  running """
@@ -422,7 +422,7 @@ zh
422
422
  URL:{{url}}
423
423
  会话:`{{session}}`
424
424
 
425
- 💡 等待当前会话完成,或使用 /solve\_stop 取消。
425
+ 💡 等待当前会话完成,或使用 /stop 取消。
426
426
  """
427
427
  issues
428
428
  list
@@ -515,7 +515,7 @@ zh
515
515
  detail "工具别名会添加 `--tool <tool>`:`/codex <github-url>` 等同于 `/solve <github-url> --tool codex`"
516
516
  reply "也可以回复包含 GitHub 链接的消息:`/solve`"
517
517
  disabled "*/solve*(别名:*/do*、*/continue*、*/claude*、*/codex*、*/opencode*、*/agent*、*/gemini*、*/qwen*)- ❌ 已禁用"
518
- queue "`/solve_queue`(别名:`/queue`)- 显示 solve 队列状态"
518
+ queue "`/queue` - 显示 solve 队列状态"
519
519
  locked
520
520
  options "🔒 锁定选项:`{{options}}`"
521
521
  task
@@ -553,7 +553,7 @@ zh
553
553
  isolation
554
554
  mode "🔒 *隔离模式:* `{{isolationBackend}}`(实验性)"
555
555
  group
556
- note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/solve\\_queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
556
+ note "⚠️ *注意:* /solve、/do、/continue、/claude、/codex、/opencode、/agent、/gemini、/qwen、/task、/split、/hive、/queue、/limits、/version、/accept\\_invites、/merge、/stop 和 /start 仅在群聊中有效。/terminal\\_watch、/watch、/subscribe 和 /unsubscribe 在私聊和群聊中有效。"
557
557
  common
558
558
  options "🔧 *常用选项:*"
559
559
  model
@@ -7,6 +7,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
7
7
  import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.lib.mjs';
8
8
  import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
+ import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
10
11
 
11
12
  /**
12
13
  * Build the user prompt for OpenCode
@@ -50,6 +51,11 @@ export const buildUserPrompt = params => {
50
51
  }
51
52
  }
52
53
 
54
+ const requestedBaseBranchDirective = buildRequestedBaseBranchDirective(argv);
55
+ if (requestedBaseBranchDirective) {
56
+ promptLines.push(requestedBaseBranchDirective);
57
+ }
58
+
53
59
  // Add blank line
54
60
  promptLines.push('');
55
61
 
@@ -119,7 +125,6 @@ CI investigation with workspace tmp directory.
119
125
  }
120
126
 
121
127
  return `You are an AI issue solver using OpenCode.
122
-
123
128
  General guidelines.
124
129
  - When you execute commands and the output becomes large, save the logs to files for easier review.
125
130
  - When running commands, avoid setting a timeout yourself. Let them run as long as needed.
@@ -282,7 +282,7 @@ export const QUEUE_CONFIG = {
282
282
 
283
283
  // Display
284
284
  // Maximum number of items shown per section (pending/processing/completed/failed)
285
- // in the /solve_queue (/queue) detailed status before collapsing into a
285
+ // in the /queue detailed status before collapsing into a
286
286
  // "... and N more" line. Keeps the Telegram message under the 4096-char cap.
287
287
  // See: https://github.com/link-assistant/hive-mind/issues/1837
288
288
  MAX_DISPLAY_ITEMS_PER_QUEUE: parseIntWithDefault('HIVE_MIND_MAX_DISPLAY_ITEMS_PER_QUEUE', 5),
@@ -7,6 +7,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
7
7
  import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.lib.mjs';
8
8
  import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
+ import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
10
11
 
11
12
  /**
12
13
  * Build the user prompt for Qwen Code
@@ -44,6 +45,11 @@ export const buildUserPrompt = params => {
44
45
  }
45
46
  }
46
47
 
48
+ const requestedBaseBranchDirective = buildRequestedBaseBranchDirective(argv);
49
+ if (requestedBaseBranchDirective) {
50
+ promptLines.push(requestedBaseBranchDirective);
51
+ }
52
+
47
53
  promptLines.push('');
48
54
 
49
55
  if (isContinueMode && feedbackLines && feedbackLines.length > 0) {
@@ -105,7 +111,6 @@ CI investigation with workspace tmp directory.
105
111
  }
106
112
 
107
113
  return `You are an AI issue solver using Qwen Code.
108
-
109
114
  General guidelines.
110
115
  - When you execute commands and the output becomes large, save the logs to files for easier review.
111
116
  - When running commands, avoid setting a timeout yourself. Let them run as long as needed.
@@ -1274,7 +1274,7 @@ export async function getRunningTrackedIsolationSessions(verbose = false, option
1274
1274
 
1275
1275
  /**
1276
1276
  * Return the currently-executing tracked sessions with the details needed to
1277
- * render them as a clickable list in `/solve_queue` (`/queue`): the issue/PR
1277
+ * render them as a clickable list in `/queue`: the issue/PR
1278
1278
  * `url`, the `tool`, the start time, and (for isolation sessions) the backend
1279
1279
  * status. Both isolation and non-isolation screen sessions are included so the
1280
1280
  * list matches what is actually executing — the queue's own in-memory
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prompt snippets for user-requested solve options.
3
+ */
4
+
5
+ function normalizeBranchName(value) {
6
+ return String(value || '').trim();
7
+ }
8
+
9
+ export function buildRequestedBaseBranchDirective(argv = {}) {
10
+ const baseBranch = normalizeBranchName(argv?.baseBranch);
11
+ if (!baseBranch) {
12
+ return '';
13
+ }
14
+
15
+ return `Requested by user --base-branch: ${baseBranch}
16
+ The user expects the pull request base branch to remain ${baseBranch}.`;
17
+ }
18
+
19
+ export const buildLockedSolveOptionsDirective = buildRequestedBaseBranchDirective;
@@ -81,6 +81,7 @@ const { maybeAttachWorkingSessionSummary, ensurePullRequestIssueLink } = results
81
81
  // Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
82
82
  const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
83
83
  const { formatAutoIterationLimit, hasReachedAutoIterationLimit, normalizeAutoIterationLimit, shouldSyncBeforeRestart } = await import('./auto-iteration-limits.lib.mjs');
84
+ const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
84
85
 
85
86
  // Issue #1895: explicitly close linked issues after merging a PR into a
86
87
  // non-default branch, where GitHub does not auto-close them.
@@ -1352,6 +1353,18 @@ export const startAutoRestartUntilMergeable = async params => {
1352
1353
  return null;
1353
1354
  }
1354
1355
 
1356
+ await ensurePullRequestBaseBranch({
1357
+ owner,
1358
+ repo,
1359
+ prNumber,
1360
+ argv,
1361
+ log,
1362
+ formatAligned,
1363
+ $,
1364
+ onMismatch: isAutoMerge ? 'throw' : 'restore',
1365
+ operation: isAutoMerge ? 'auto-merge' : 'auto-restart-until-mergeable',
1366
+ });
1367
+
1355
1368
  // Issue #1226: Check if running in fork mode — auto-merge cannot work without write access
1356
1369
  if (argv.fork && isAutoMerge) {
1357
1370
  await log('');
@@ -890,7 +890,7 @@ _Details will be added as the solution draft is developed..._
890
890
 
891
891
  if (argv.verbose) {
892
892
  await log(` PR Title: [WIP] ${issueTitle}`, { verbose: true });
893
- await log(` Base branch: ${defaultBranch}`, { verbose: true });
893
+ await log(` Base branch: ${targetBranch}`, { verbose: true });
894
894
  await log(` Head branch: ${branchName}`, { verbose: true });
895
895
  if (currentUser) {
896
896
  await log(` Assignee: ${currentUser}`, { verbose: true });
@@ -0,0 +1,21 @@
1
+ export async function finalizeSolveProcess({ tempDir, argv, limitReached, path, getLogFile, log, closeSentry, logActiveHandles, cleanupTempDirectory, safeExit }) {
2
+ await cleanupTempDirectory(tempDir, argv, limitReached);
3
+
4
+ // Show final log file reference so users always know where to find the complete log
5
+ if (getLogFile()) {
6
+ const finalLogPath = path.resolve(getLogFile());
7
+ await log(`\n📁 Complete log file: ${finalLogPath}`);
8
+ }
9
+
10
+ // Issue #1346: Flush Sentry events before exit.
11
+ // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
12
+ await closeSentry();
13
+
14
+ // Issue #1431: Log active handles before draining.
15
+ // Always logged to file and console so future hangs are immediately visible in logs.
16
+ // drainHandles() inside safeExit() will unref/close these before process.exit().
17
+ await logActiveHandles(msg => log(msg));
18
+
19
+ // Issue #1431: safeExit() unrefs handles so the event loop exits naturally, then calls process.exit(0)
20
+ await safeExit(0, 'Process completed');
21
+ }
package/src/solve.mjs CHANGED
@@ -47,6 +47,7 @@ const { startAutoRestartUntilMergeable } = await import('./solve.auto-merge.lib.
47
47
  const { runAutoEnsureRequirements } = await import('./solve.auto-ensure.lib.mjs');
48
48
  const { runKeepWorkingUntilDone } = await import('./solve.keep-working.lib.mjs');
49
49
  const { runEscalation } = await import('./solve.escalate.lib.mjs');
50
+ const { finalizeSolveProcess } = await import('./solve.finalize.lib.mjs');
50
51
  const exitHandler = await import('./exit-handler.lib.mjs');
51
52
  const { initializeExitHandler, installGlobalExitHandlers, safeExit, logActiveHandles } = exitHandler;
52
53
  const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
@@ -54,6 +55,7 @@ const { createInterruptWrapper } = await import('./solve.interrupt.lib.mjs');
54
55
  const { configureWorkingSession, beginWorkingSession, endWorkingSession } = await import('./working-session.lib.mjs');
55
56
  const getResourceSnapshot = memoryCheck.getResourceSnapshot;
56
57
  const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
58
+ const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
57
59
  const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
58
60
  const { recordAfterCloneSize, recordAfterAgentSize } = await import('./solve.disk-diagnostics.lib.mjs');
59
61
  const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
@@ -617,6 +619,10 @@ try {
617
619
  await handleNoPrAvailableError({ isContinueMode, tempDir, issueNumber, issueUrl, owner, repo, log, formatAligned });
618
620
  }
619
621
 
622
+ const enforceRequestedBaseBranch = () => ensurePullRequestBaseBranch({ owner, repo, prNumber, argv, log, formatAligned, $ });
623
+
624
+ await enforceRequestedBaseBranch();
625
+
620
626
  if (isContinueMode) {
621
627
  await log(`\n${formatAligned('🔄', 'Continue mode:', 'ACTIVE')}`);
622
628
  await log(formatAligned('', 'Using existing PR:', `#${prNumber}`, 2));
@@ -1203,6 +1209,8 @@ try {
1203
1209
  await safeExit(0, 'Auto-continue child process will handle post-processing');
1204
1210
  }
1205
1211
 
1212
+ await enforceRequestedBaseBranch();
1213
+
1206
1214
  // Issue #1263 / #1728: Working session summary attachment.
1207
1215
  // Routed through the shared maybeAttachWorkingSessionSummary helper so that
1208
1216
  // top-level solve, auto-restart-until-mergeable, and watch-mode iterations
@@ -1329,6 +1337,8 @@ try {
1329
1337
  }
1330
1338
  }
1331
1339
 
1340
+ await enforceRequestedBaseBranch();
1341
+
1332
1342
  // Track whether logs were successfully attached (used by endWorkSession)
1333
1343
  let logsAttached = false;
1334
1344
 
@@ -1475,23 +1485,5 @@ try {
1475
1485
  $,
1476
1486
  });
1477
1487
  } finally {
1478
- await cleanupTempDirectory(tempDir, argv, limitReached);
1479
-
1480
- // Show final log file reference so users always know where to find the complete log
1481
- if (getLogFile()) {
1482
- const finalLogPath = path.resolve(getLogFile());
1483
- await log(`\n📁 Complete log file: ${finalLogPath}`);
1484
- }
1485
-
1486
- // Issue #1346: Flush Sentry events before exit.
1487
- // closeSentry() uses a hard Promise.race deadline so it cannot block indefinitely.
1488
- await closeSentry();
1489
-
1490
- // Issue #1431: Log active handles before draining.
1491
- // Always logged to file and console so future hangs are immediately visible in logs.
1492
- // drainHandles() inside safeExit() will unref/close these before process.exit().
1493
- await logActiveHandles(msg => log(msg));
1494
-
1495
- // Issue #1431: safeExit() unrefs handles so the event loop exits naturally, then calls process.exit(0)
1496
- await safeExit(0, 'Process completed');
1488
+ await finalizeSolveProcess({ tempDir, argv, limitReached, path, getLogFile, log, closeSentry, logActiveHandles, cleanupTempDirectory, safeExit });
1497
1489
  }
@@ -0,0 +1,62 @@
1
+ import { buildPullRequestBaseBranchInterventionMessage, detectForbiddenPullRequestBaseChangeCommand, extractToolCommandTextsFromStreamEvent } from './solve.pr-base-guard.lib.mjs';
2
+
3
+ export function createPullRequestBaseBranchCommandIntervention({ expectedBaseBranch, prNumber, log = async () => {}, toolLabel = 'AI tool', sendInput = null, stopSession = null } = {}) {
4
+ const observedCommands = new Set();
5
+ let intervention = null;
6
+ let sent = false;
7
+
8
+ const handleCommand = async command => {
9
+ if (!expectedBaseBranch || intervention || sent || !command || observedCommands.has(command)) return;
10
+ observedCommands.add(command);
11
+
12
+ const violation = detectForbiddenPullRequestBaseChangeCommand(command, {
13
+ expectedBaseBranch,
14
+ prNumber,
15
+ });
16
+ if (!violation) return;
17
+
18
+ const message = buildPullRequestBaseBranchInterventionMessage(violation);
19
+ await log(`\n⚠️ Forbidden PR base retarget command observed from ${toolLabel}: ${command}`, { level: 'warning' });
20
+
21
+ if (sendInput) {
22
+ try {
23
+ sent = await sendInput(message);
24
+ } catch (sendError) {
25
+ await log(` Could not send requested base-branch correction to ${toolLabel} input: ${sendError.message}`, { level: 'warning' });
26
+ }
27
+ if (sent) {
28
+ await log(` Sent requested base-branch correction to ${toolLabel} input.`, { level: 'warning' });
29
+ return;
30
+ }
31
+ }
32
+
33
+ intervention = { violation, message };
34
+ await log(` ${message}`, { level: 'warning' });
35
+ if (stopSession) {
36
+ try {
37
+ const stopped = await stopSession();
38
+ if (stopped) {
39
+ await log(` Stopped ${toolLabel} session to resume with requested base-branch correction.`, { level: 'warning' });
40
+ }
41
+ } catch (stopError) {
42
+ await log(` Could not stop ${toolLabel} process for immediate correction: ${stopError.message}`, { level: 'warning' });
43
+ }
44
+ }
45
+ };
46
+
47
+ const handleCommands = async commands => {
48
+ for (const command of commands) {
49
+ await handleCommand(command);
50
+ if (intervention || sent) return;
51
+ }
52
+ };
53
+
54
+ return {
55
+ handleCommand,
56
+ handleCommands,
57
+ handleCommandExecutions: async commandExecutions => handleCommands((commandExecutions || []).map(commandExecution => commandExecution?.command).filter(Boolean)),
58
+ handleStreamEvent: async event => handleCommands(extractToolCommandTextsFromStreamEvent(event)),
59
+ getIntervention: () => intervention,
60
+ wasSent: () => sent,
61
+ };
62
+ }