@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.0.28
4
+
5
+ ### Patch Changes
6
+
7
+ - 5b4f3df: Recommend and accept `/queue` instead of the legacy solve-prefixed queue commands, and recommend `/stop` for cancelling running sessions.
8
+
9
+ ## 2.0.27
10
+
11
+ ### Patch Changes
12
+
13
+ - d3fdb7b: Respect explicit `--base-branch` through solve sessions by instructing agents not to retarget PRs and restoring the requested PR base before verification or auto-merge handling.
14
+
3
15
  ## 2.0.26
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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 Agent
@@ -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
 
@@ -790,6 +790,20 @@ export const createBidirectionalHandler = options => {
790
790
  return writeFrameToStdin(state.claudeStdin, frame, log, verbose);
791
791
  };
792
792
 
793
+ /**
794
+ * Stream a non-comment feedback message into the attached Claude stdin.
795
+ *
796
+ * @param {string} feedbackText
797
+ * @param {Object} [options]
798
+ * @param {string} [options.kind='metadata']
799
+ * @returns {Promise<boolean>} Whether the write succeeded
800
+ */
801
+ const sendFeedback = async (feedbackText, options = {}) => {
802
+ if (!state.claudeStdin) return false;
803
+ const frame = formatFeedbackForClaude(feedbackText, { kind: options.kind || 'metadata' });
804
+ return writeFrameToStdin(state.claudeStdin, frame, log, verbose);
805
+ };
806
+
793
807
  /**
794
808
  * Get current handler state (for debugging)
795
809
  *
@@ -828,6 +842,7 @@ export const createBidirectionalHandler = options => {
828
842
  attachClaudeStdin,
829
843
  detachClaudeStdin,
830
844
  streamInitialPrompt,
845
+ sendFeedback,
831
846
  // Issue #1708: queue mode + status streaming
832
847
  markAiBusy,
833
848
  markAiIdle,
@@ -34,11 +34,10 @@ import { createThinkingBlockRecovery } from './claude.thinking-block-recovery.li
34
34
  import { buildMissingClaudeResultMessage, collectClaudeStreamEventFacts, getClaudeMessageContent, shouldFailClaudeStreamWithoutResult } from './claude.stream-events.lib.mjs';
35
35
  import { formatNumber, mapModelToId, checkModelVisionCapability } from './claude.model-utils.lib.mjs';
36
36
  import { showResumeCommand } from './claude.resume-output.lib.mjs';
37
+ import { createPullRequestBaseBranchCommandIntervention } from './solve.pr-base-command-intervention.lib.mjs';
37
38
  export { availableModels, fetchModelInfo }; // Re-export for backward compatibility
38
39
  export { formatNumber, mapModelToId, checkModelVisionCapability };
39
- // Function to validate Claude CLI connection with retry logic
40
40
  export const validateClaudeConnection = async (model = 'haiku') => {
41
- // Map model alias to full ID
42
41
  const mappedModel = mapModelToId(model);
43
42
  const maxRetries = 3;
44
43
  const baseDelay = timeouts.retryBaseDelay;
@@ -60,14 +59,12 @@ export const validateClaudeConnection = async (model = 'haiku') => {
60
59
  }
61
60
  }
62
61
  } catch (versionError) {
63
- // Version check failed, but we'll continue with the main validation
64
62
  if (retryCount === 0) {
65
63
  await log(`āš ļø Claude CLI version check failed (${versionError.code}), proceeding with connection test...`);
66
64
  }
67
65
  }
68
66
  let result;
69
67
  try {
70
- // Primary validation: use printf piping with specified model
71
68
  result = await $`printf hi | claude --model ${mappedModel} -p`;
72
69
  } catch (pipeError) {
73
70
  await log(`āš ļø Pipe validation failed (${pipeError.code}), trying timeout approach...`);
@@ -109,9 +106,7 @@ export const validateClaudeConnection = async (model = 'haiku') => {
109
106
  return null;
110
107
  };
111
108
  const jsonError = checkForJsonError(stdout) || checkForJsonError(stderr);
112
- // Check for API overload error pattern (Issue #1439: also detect 529 overloaded_error)
113
109
  const isOverloadError = (stdout.includes('API Error: 500') && stdout.includes('Overloaded')) || (stdout.includes('API Error: 529') && stdout.includes('Overloaded')) || (stderr.includes('API Error: 500') && stderr.includes('Overloaded')) || (stderr.includes('API Error: 529') && stderr.includes('Overloaded')) || (jsonError && (jsonError.type === 'api_error' || jsonError.type === 'overloaded_error') && jsonError.message === 'Overloaded');
114
- // Handle overload errors with retry
115
110
  if (isOverloadError) {
116
111
  if (retryCount < maxRetries) {
117
112
  const delay = baseDelay * Math.pow(2, retryCount);
@@ -187,8 +182,7 @@ export const validateClaudeConnection = async (model = 'haiku') => {
187
182
  await log(' šŸ’” Make sure Claude CLI is installed and accessible', { level: 'error' });
188
183
  return false;
189
184
  }
190
- }; // End of attemptValidation function
191
- // Start the validation with retry logic
185
+ };
192
186
  return await attemptValidation();
193
187
  };
194
188
  export { handleClaudeRuntimeSwitch }; // Re-export from ./claude.runtime-switch.lib.mjs
@@ -203,18 +197,15 @@ export const setClaudeVersion = version => {
203
197
  /** Resolve thinking settings based on --think and --thinking-budget options */
204
198
  export const resolveThinkingSettings = async (argv, log) => {
205
199
  const minVersion = argv.thinkingBudgetClaudeMinimumVersion || '2.1.12';
206
- const version = detectedClaudeVersion || '0.0.0'; // Assume old version if not detected
200
+ const version = detectedClaudeVersion || '0.0.0';
207
201
  const isNewVersion = supportsThinkingBudget(version, minVersion);
208
- // Get max thinking budget from argv or use default (see issue #1146)
209
202
  const maxBudget = argv.maxThinkingBudget ?? DEFAULT_MAX_THINKING_BUDGET;
210
- // Get thinking level mappings calculated from maxBudget
211
203
  const thinkingLevelToTokens = getThinkingLevelToTokens(maxBudget);
212
204
  const tokensToThinkingLevel = getTokensToThinkingLevel(maxBudget);
213
205
  let thinkingBudget = argv.thinkingBudget;
214
206
  let thinkLevel = argv.think;
215
207
  let translation = null;
216
208
  if (isNewVersion) {
217
- // Claude Code >= 2.1.12: translate --think to --thinking-budget
218
209
  if (thinkLevel !== undefined && thinkingBudget === undefined) {
219
210
  thinkingBudget = thinkingLevelToTokens[thinkLevel];
220
211
  translation = `--think ${thinkLevel} → --thinking-budget ${thinkingBudget}`;
@@ -227,7 +218,6 @@ export const resolveThinkingSettings = async (argv, log) => {
227
218
  }
228
219
  }
229
220
  } else {
230
- // Claude Code < 2.1.12: translate --thinking-budget to --think keywords
231
221
  if (thinkingBudget !== undefined && thinkLevel === undefined) {
232
222
  thinkLevel = tokensToThinkingLevel(thinkingBudget);
233
223
  translation = `--thinking-budget ${thinkingBudget} → --think ${thinkLevel}`;
@@ -235,7 +225,6 @@ export const resolveThinkingSettings = async (argv, log) => {
235
225
  await log(`šŸ“Š Translating for Claude Code ${version} (< ${minVersion}):`, { verbose: true });
236
226
  await log(` ${translation}`, { verbose: true });
237
227
  }
238
- // Clear thinkingBudget since old versions don't support it
239
228
  thinkingBudget = undefined;
240
229
  }
241
230
  }
@@ -243,10 +232,8 @@ export const resolveThinkingSettings = async (argv, log) => {
243
232
  };
244
233
  /** Check if Playwright MCP is available and connected to Claude @returns {Promise<boolean>} */
245
234
  export const checkPlaywrightMcpAvailability = ensureClaudePlaywrightMcpServer;
246
- /** Execute Claude with all prompts and settings - main entry point */
247
235
  export const executeClaude = async params => {
248
236
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
249
- // Check if agent-commander is installed when the option is enabled
250
237
  if (argv.promptSubagentsViaAgentCommander) {
251
238
  try {
252
239
  await $`which start-agent`;
@@ -256,9 +243,7 @@ export const executeClaude = async params => {
256
243
  await log('āš ļø agent-commander not installed; prompt guidance will be skipped (npm i -g @link-assistant/agent-commander)');
257
244
  }
258
245
  }
259
- // Import prompt building functions from claude.prompts.lib.mjs
260
246
  const { buildUserPrompt, buildSystemPrompt } = await import('./claude.prompts.lib.mjs');
261
- // Check if the model supports vision using models.dev API
262
247
  const mappedModel = mapModelToId(argv.model);
263
248
  const modelSupportsVision = await checkModelVisionCapability(mappedModel);
264
249
  if (argv.verbose) {
@@ -304,7 +289,6 @@ export const executeClaude = async params => {
304
289
  if (feedbackLines && feedbackLines.length > 0) {
305
290
  await log(' Feedback info: Included', { verbose: true });
306
291
  }
307
- // In dry-run mode, output the actual prompts for debugging
308
292
  if (argv.dryRun) {
309
293
  await log('\nšŸ“‹ User prompt content:', { verbose: true });
310
294
  await log('---BEGIN USER PROMPT---', { verbose: true });
@@ -359,41 +343,30 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
359
343
  const os = (await use('os')).default;
360
344
  const homeDir = options.homeDir || os.homedir();
361
345
  const fetchModelInfoForUsage = options.fetchModelInfo || fetchModelInfo;
362
- // Construct the path to the session JSONL file
363
- // Format: ~/.claude/projects/<project-dir>/<session-id>.jsonl
364
- // The project directory name is the full path with slashes replaced by dashes
365
- // e.g., /tmp/gh-issue-solver-123 becomes -tmp-gh-issue-solver-123
366
346
  const projectDirName = tempDir.replace(/\//g, '-');
367
347
  const sessionFile = path.join(homeDir, '.claude', 'projects', projectDirName, `${sessionId}.jsonl`);
368
348
  try {
369
349
  await fs.access(sessionFile);
370
350
  } catch {
371
- // File doesn't exist yet or can't be accessed
372
351
  return null;
373
352
  }
374
- // Initialize per-model usage tracking
375
353
  const modelUsage = {};
376
354
  // Issue #1501: Deduplicate JSONL entries by message ID (stream-json splits responses)
377
355
  const seenMessageIds = new Set();
378
356
  let duplicateCount = 0;
379
- // Issue #1501: Track peak context usage per request (not cumulative)
380
357
  const peakContextByModel = {};
381
358
  let globalPeakContext = 0;
382
- // Issue #1491: Track sub-sessions between compactification events
383
359
  const subSessions = [];
384
360
  let currentSubSession = createEmptySubSessionUsage();
385
361
  const compactifications = [];
386
362
  try {
387
- // Read the entire file
388
363
  const fileContent = await fs.readFile(sessionFile, 'utf8');
389
364
  const lines = fileContent.trim().split('\n');
390
365
  for (const line of lines) {
391
366
  if (!line.trim()) continue;
392
367
  try {
393
368
  const entry = JSON.parse(line);
394
- // Issue #1491: Detect compactification boundary events
395
369
  if (entry.type === 'system' && entry.subtype === 'compact_boundary') {
396
- // Save current sub-session and start a new one
397
370
  if (currentSubSession.messageCount > 0) {
398
371
  subSessions.push(currentSubSession);
399
372
  }
@@ -411,7 +384,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
411
384
  if (msgId) {
412
385
  if (seenMessageIds.has(msgId)) {
413
386
  duplicateCount++;
414
- continue; // Skip — already counted this message's usage
387
+ continue;
415
388
  }
416
389
  seenMessageIds.add(msgId);
417
390
  }
@@ -429,7 +402,6 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
429
402
  if (requestContext > globalPeakContext) {
430
403
  globalPeakContext = requestContext;
431
404
  }
432
- // Issue #1491: Also track per-sub-session usage
433
405
  if (usage.input_tokens) currentSubSession.inputTokens += usage.input_tokens;
434
406
  if (usage.cache_creation_input_tokens) currentSubSession.cacheCreationTokens += usage.cache_creation_input_tokens;
435
407
  if (usage.cache_read_input_tokens) currentSubSession.cacheReadTokens += usage.cache_read_input_tokens;
@@ -448,16 +420,13 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
448
420
  continue;
449
421
  }
450
422
  }
451
- // Push the final sub-session
452
423
  if (currentSubSession.messageCount > 0) {
453
424
  subSessions.push(currentSubSession);
454
425
  }
455
426
  mergeResultModelUsage(modelUsage, resultModelUsage);
456
- // If no usage data was found, return null
457
427
  if (Object.keys(modelUsage).length === 0) {
458
428
  return null;
459
429
  }
460
- // Fetch model information for each model
461
430
  const modelInfoPromises = Object.keys(modelUsage).map(async modelId => {
462
431
  const modelInfo = await fetchModelInfoForUsage(modelId);
463
432
  return { modelId, modelInfo };
@@ -469,7 +438,6 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
469
438
  modelInfoMap[modelId] = modelInfo;
470
439
  }
471
440
  }
472
- // Calculate cost for each model and store all characteristics
473
441
  for (const [modelId, usage] of Object.entries(modelUsage)) {
474
442
  const modelInfo = modelInfoMap[modelId];
475
443
  // Issue #1501: Attach peak context usage per model
@@ -480,7 +448,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
480
448
  usage.costUSD = costData.total;
481
449
  usage.costBreakdown = costData.breakdown;
482
450
  usage.modelName = modelInfo.name || modelId;
483
- usage.modelInfo = modelInfo; // Store complete model info
451
+ usage.modelInfo = modelInfo;
484
452
  } else {
485
453
  usage.costUSD = usage._resultCostUSD ?? null;
486
454
  usage.costBreakdown = null;
@@ -491,7 +459,6 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
491
459
  usage.modelInfo = ctx || out ? { limit: { context: ctx || null, output: out || null } } : null;
492
460
  }
493
461
  }
494
- // Calculate grand totals across all models
495
462
  let totalInputTokens = 0;
496
463
  let totalCacheCreationTokens = 0;
497
464
  let totalCacheReadTokens = 0;
@@ -508,12 +475,9 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
508
475
  hasCostData = true;
509
476
  }
510
477
  }
511
- // Calculate total tokens (input + cache_creation + output, cache_read doesn't count as new tokens)
512
478
  const totalTokens = totalInputTokens + totalCacheCreationTokens + totalOutputTokens;
513
479
  return {
514
- // Per-model breakdown
515
480
  modelUsage,
516
- // Grand totals
517
481
  inputTokens: totalInputTokens,
518
482
  cacheCreationTokens: totalCacheCreationTokens,
519
483
  cacheReadTokens: totalCacheReadTokens,
@@ -540,7 +504,6 @@ export const executeClaudeCommand = async params => {
540
504
  branchName,
541
505
  prompt,
542
506
  systemPrompt,
543
- escapedPrompt,
544
507
  escapedSystemPrompt,
545
508
  argv,
546
509
  log,
@@ -560,22 +523,19 @@ export const executeClaudeCommand = async params => {
560
523
  // and issue body/title polling in setupBidirectionalHandler.
561
524
  issueNumber,
562
525
  } = params;
563
- // Issue #817: Apply bidirectional-mode composition and tool-support validation before running.
564
- // This may enable argv.interactiveMode, argv.acceptIncommingCommentsAsInput, and
565
- // argv.excludeAllOwnIncommingCommentsFromInput when --bidirectional-interactive-mode is set.
526
+ const expectedBaseBranch = String(argv?.baseBranch || '').trim();
527
+ const escapePromptForShell = promptText => String(promptText).replace(/"/g, '\\"').replace(/\$/g, '\\$');
566
528
  await validateBidirectionalModeConfig(argv, log);
567
- // Issue #1331: Unified retry configuration for all transient API errors
568
- // (Overloaded, 503 Network Error, Internal Server Error) - same params, all with session preservation
569
529
  let retryCount = 0;
530
+ let baseBranchInterventionPrompt = null;
531
+ let baseBranchInterventionResumeCount = 0;
570
532
  // Issue #1834 (PR #1835 feedback): corrupted-thinking-block recovery — resume the session first,
571
533
  // then escalate to a fresh restart, auto-committing uncommitted work before each attempt. Created
572
534
  // once so its resume/restart caps persist across recursive retry calls.
573
535
  const tryThinkingBlockRecovery = createThinkingBlockRecovery({ argv, tempDir, branchName, $, log });
574
- // Helper `waitWithCountdown` (per-minute countdown for delays >1 minute, Issue #1331) is shared
575
- // from tool-retry.lib.mjs so claude/codex/gemini/qwen/opencode all use one implementation.
576
- // Function to execute with retry logic
577
536
  const executeWithRetry = async () => {
578
- // Execute claude command from the cloned repository directory
537
+ const promptForAttempt = baseBranchInterventionPrompt ? `${prompt}\n\n${baseBranchInterventionPrompt}\n` : prompt;
538
+ const escapedPromptForAttempt = escapePromptForShell(promptForAttempt);
579
539
  if (retryCount === 0) {
580
540
  await log(`\n${formatAligned('šŸ¤–', 'Executing Claude:', argv.model.toUpperCase())}`);
581
541
  } else {
@@ -585,7 +545,7 @@ export const executeClaudeCommand = async params => {
585
545
  // Issue #1949: logExecutionContext shows the requested alias with its resolved
586
546
  // full ID (e.g. "opus (claude-opus-4-8)"). The old `argv.model === 'opus' ?
587
547
  // 'opus' : 'sonnet'` heuristic mislabelled every non-"opus" alias as "sonnet".
588
- await logExecutionContext({ log, model: argv.model, tool: 'claude', tempDir, branchName, promptLength: prompt.length, systemPromptLength: systemPrompt.length, feedbackLines });
548
+ await logExecutionContext({ log, model: argv.model, tool: 'claude', tempDir, branchName, promptLength: promptForAttempt.length, systemPromptLength: systemPrompt.length, feedbackLines });
589
549
  }
590
550
  const resourcesBefore = await getResourceSnapshot();
591
551
  await log('šŸ“ˆ System resources before execution:', { verbose: true });
@@ -628,7 +588,6 @@ export const executeClaudeCommand = async params => {
628
588
  outputTokens: 0,
629
589
  eventCount: 0,
630
590
  };
631
- // Create interactive mode handler if enabled
632
591
  let interactiveHandler = null;
633
592
  if (argv.interactiveMode && owner && repo && prNumber) {
634
593
  await log('šŸ”Œ Interactive mode: Creating handler for real-time PR comments', { verbose: true });
@@ -649,10 +608,6 @@ export const executeClaudeCommand = async params => {
649
608
  } else if (argv.interactiveMode) {
650
609
  await log('āš ļø Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
651
610
  }
652
- // Issue #817 / #1708: Set up bidirectional handler when --accept-incomming-comments-as-input
653
- // (or composite --bidirectional-interactive-mode / --auto-input-until-mergeable) is enabled.
654
- // Returns null when inactive. issueNumber + tempDir are forwarded so the handler can
655
- // poll issue title/body changes and uncommitted changes during the session (Issue #1708).
656
611
  const bidirectionalHandler = await setupBidirectionalHandler({ argv, owner, repo, prNumber, issueNumber, tempDir, $, log });
657
612
  const progressMonitor = await initProgressMonitoring(argv, { owner, repo, prNumber, $, log }); // works with or without --interactive-mode
658
613
  let execCommand;
@@ -668,7 +623,6 @@ export const executeClaudeCommand = async params => {
668
623
  const useClaudeFallbackModel = !resolvedPlanModel && mappedFallbackModel && mappedFallbackModel !== effectiveModel;
669
624
  let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
670
625
  if (useClaudeFallbackModel) claudeArgs += ` --fallback-model ${mappedFallbackModel}`;
671
- // Declare queuedFeedback for use in catch/finally blocks and return value
672
626
  let queuedFeedback = [];
673
627
  // Issue #817: When --accept-incomming-comments-as-input is set and we are
674
628
  // not resuming a prior session, drive Claude via NDJSON stream-json input
@@ -692,19 +646,18 @@ export const executeClaudeCommand = async params => {
692
646
  // Prompt is delivered as the first NDJSON frame on stdin (not as -p).
693
647
  claudeArgs += ` -p --input-format stream-json --append-system-prompt "${escapedSystemPrompt}"`;
694
648
  } else {
695
- claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
649
+ claudeArgs += ` -p "${escapedPromptForAttempt}" --append-system-prompt "${escapedSystemPrompt}"`;
696
650
  }
697
651
  const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
698
652
  await log(`\n${formatAligned('šŸ“', 'Raw command:', '')}`);
699
653
  await log(`${fullCommand}`);
700
654
  await log('');
701
655
  if (argv.verbose) {
702
- await log(`šŸ“‹ User prompt:\n---BEGIN USER PROMPT---\n${prompt}\n---END USER PROMPT---`, { verbose: true });
656
+ await log(`šŸ“‹ User prompt:\n---BEGIN USER PROMPT---\n${promptForAttempt}\n---END USER PROMPT---`, { verbose: true });
703
657
  await log(`šŸ“‹ System prompt:\n---BEGIN SYSTEM PROMPT---\n${systemPrompt}\n---END SYSTEM PROMPT---`, { verbose: true });
704
658
  }
705
659
  try {
706
660
  const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
707
- // Issue #1706: --sub-session-size + --disable-1m-context. Resolve here, then pass into getClaudeEnv along with the rest.
708
661
  const { parsed: parsedSubSessionSize, contextWindowTokens } = await resolveSubSessionSize({ rawValue: argv.subSessionSize, tool: 'claude', modelId: effectiveModel, fetchModelInfo, log });
709
662
  // Issue #817: streaming mode sets exitAfterStopDelayMs=60000 so the headless Claude process stays alive between NDJSON turns.
710
663
  const claudeEnv = getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel, subAgentModel: resolvedSubAgentModel, showThinkingContent: argv.showThinkingContent, exitAfterStopDelayMs: streamingInput ? 60_000 : undefined, disable1mContext: !!argv.disable1mContext, subSessionSize: parsedSubSessionSize, contextWindowTokens });
@@ -728,7 +681,7 @@ export const executeClaudeCommand = async params => {
728
681
  const fallbackModelArgs = useClaudeFallbackModel ? ['--fallback-model', mappedFallbackModel] : []; // Issue #1949: Claude Code's per-request overload fallback
729
682
  if (useClaudeFallbackModel && argv.verbose) await log(`šŸ“Š Claude --fallback-model: ${mappedFallbackModel} (Issue #1949 — primary --model ${effectiveModel} stays stable across overload retries)`, { verbose: true });
730
683
  if (argv.resume) {
731
- const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
684
+ const simpleEscapedPrompt = promptForAttempt.replace(/"/g, '\\"');
732
685
  execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
733
686
  } else if (streamingInput) {
734
687
  // Issue #817: Drive Claude via --input-format stream-json on a pipe
@@ -737,10 +690,10 @@ export const executeClaudeCommand = async params => {
737
690
  const streamingInputArgs = ['-p', '--input-format', 'stream-json'];
738
691
  execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
739
692
  } else {
740
- execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
693
+ execCommand = $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
741
694
  }
742
695
  if (streamingInput) {
743
- await attachStreamingInput(bidirectionalHandler, execCommand, prompt, log, !!argv.verbose);
696
+ await attachStreamingInput(bidirectionalHandler, execCommand, promptForAttempt, log, !!argv.verbose);
744
697
  }
745
698
  await log(`${formatAligned('šŸ“‹', 'Command details:', '')}`);
746
699
  await log(formatAligned('šŸ“‚', 'Working directory:', tempDir, 2));
@@ -815,6 +768,19 @@ export const executeClaudeCommand = async params => {
815
768
  activityTimeoutId.unref();
816
769
  }
817
770
  };
771
+ const baseBranchCommandIntervention = createPullRequestBaseBranchCommandIntervention({
772
+ expectedBaseBranch,
773
+ prNumber,
774
+ log,
775
+ toolLabel: 'Claude',
776
+ sendInput: message => (streamingInput && bidirectionalHandler?.sendFeedback ? bidirectionalHandler.sendFeedback(message, { kind: 'metadata' }) : false),
777
+ stopSession: async () => {
778
+ if (forceExitTriggered || !execCommand?.kill) return false;
779
+ forceExitTriggered = true;
780
+ killProcessTree('SIGTERM');
781
+ return true;
782
+ },
783
+ });
818
784
  for await (const chunk of execCommand.stream()) {
819
785
  // Issue #1510: Continue processing stream after SIGTERM to capture final output
820
786
  // The stream will naturally end when the process exits (SIGTERM) or is force-killed (SIGKILL after 5s)
@@ -854,6 +820,7 @@ export const executeClaudeCommand = async params => {
854
820
  }
855
821
  }
856
822
  await log(JSON.stringify(data, null, 2));
823
+ await baseBranchCommandIntervention.handleStreamEvent(data);
857
824
  if (!sessionId && data.session_id) {
858
825
  sessionId = data.session_id;
859
826
  await log(`šŸ“Œ Session ID: ${sessionId}`);
@@ -1088,6 +1055,7 @@ export const executeClaudeCommand = async params => {
1088
1055
  try {
1089
1056
  const data = sanitizeObjectStrings(JSON.parse(stdoutLineBuffer));
1090
1057
  await log(JSON.stringify(data, null, 2));
1058
+ await baseBranchCommandIntervention.handleStreamEvent(data);
1091
1059
  const eventFacts = collectClaudeStreamEventFacts(data);
1092
1060
  messageCount += eventFacts.messageCountDelta;
1093
1061
  toolUseCount += eventFacts.toolUseCountDelta;
@@ -1167,6 +1135,34 @@ export const executeClaudeCommand = async params => {
1167
1135
 
1168
1136
  // Issue #817: Stop bidirectional mode monitoring and collect queued feedback
1169
1137
  queuedFeedback = await finalizeBidirectionalHandler(bidirectionalHandler, log);
1138
+ const baseBranchIntervention = baseBranchCommandIntervention.getIntervention();
1139
+ if (baseBranchIntervention && !baseBranchCommandIntervention.wasSent()) {
1140
+ if ((sessionId || argv.resume) && baseBranchInterventionResumeCount < 1) {
1141
+ argv.resume = sessionId || argv.resume;
1142
+ baseBranchInterventionPrompt = baseBranchIntervention.message;
1143
+ baseBranchInterventionResumeCount++;
1144
+ await log('\nšŸ”„ Resuming Claude with requested base-branch correction prompt...');
1145
+ return await executeWithRetry();
1146
+ }
1147
+
1148
+ return {
1149
+ success: false,
1150
+ sessionId,
1151
+ limitReached,
1152
+ limitResetTime,
1153
+ limitTimezone,
1154
+ messageCount,
1155
+ toolUseCount,
1156
+ errorDuringExecution,
1157
+ anthropicTotalCostUSD,
1158
+ resultSummary,
1159
+ errorInfo: {
1160
+ message: baseBranchIntervention.message,
1161
+ violation: baseBranchIntervention.violation,
1162
+ },
1163
+ queuedFeedback,
1164
+ };
1165
+ }
1170
1166
  const retryableLastError = classifyRetryableError(lastMessage);
1171
1167
  // Issue #1834: Corrupted extended-thinking blocks → try to resume the session first, then fall
1172
1168
  // back to a fresh restart (PR #1835 feedback). When both caps are reached, tryThinkingBlockRecovery
@@ -9,6 +9,7 @@ import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.
9
9
  import { primaryModelNames } from './models/index.mjs';
10
10
  import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
11
11
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
12
+ import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
12
13
 
13
14
  /**
14
15
  * Build the user prompt for Claude
@@ -57,6 +58,11 @@ export const buildUserPrompt = params => {
57
58
  }
58
59
  }
59
60
 
61
+ const requestedBaseBranchDirective = buildRequestedBaseBranchDirective(argv);
62
+ if (requestedBaseBranchDirective) {
63
+ promptLines.push(requestedBaseBranchDirective);
64
+ }
65
+
60
66
  // Add contributing guidelines if available
61
67
  if (contributingGuidelines) {
62
68
  promptLines.push('');
package/src/codex.lib.mjs CHANGED
@@ -35,6 +35,7 @@ import { classifyRetryableError, getRetryDelayMs, maybeSwitchToFallbackModel, wa
35
35
  import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
36
36
  import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
37
37
  import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
38
+ import { createPullRequestBaseBranchCommandIntervention } from './solve.pr-base-command-intervention.lib.mjs';
38
39
  import Decimal from 'decimal.js-light';
39
40
 
40
41
  const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'reasoning_output_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
@@ -767,9 +768,12 @@ export const executeCodexCommand = async params => {
767
768
  const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, codexPath, $, owner, repo, prNumber, calculatePricing = calculateCodexPricing, waitForRetryDelay = waitWithCountdown } = params;
768
769
 
769
770
  const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
771
+ const expectedBaseBranch = String(argv?.baseBranch || '').trim();
770
772
 
771
773
  // Retry configuration
772
774
  let retryCount = 0;
775
+ let baseBranchInterventionPrompt = null;
776
+ let baseBranchInterventionResumeCount = 0;
773
777
 
774
778
  const executeWithRetry = async () => {
775
779
  // Execute codex command from the cloned repository directory
@@ -806,7 +810,8 @@ export const executeCodexCommand = async params => {
806
810
 
807
811
  // For Codex, we combine system and user prompts into a single message
808
812
  // Codex doesn't have separate system prompt support in CLI mode
809
- const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt;
813
+ const promptForAttempt = baseBranchInterventionPrompt ? `${prompt}\n\n${baseBranchInterventionPrompt}\n` : prompt;
814
+ const combinedPrompt = systemPrompt ? `${systemPrompt}\n\n${promptForAttempt}` : promptForAttempt;
810
815
 
811
816
  // Write the combined prompt to a file for piping
812
817
  // Use OS temporary directory instead of repository workspace to avoid polluting the repo
@@ -919,6 +924,17 @@ export const executeCodexCommand = async params => {
919
924
  let lastMessage = '';
920
925
  let lastTextContent = ''; // Issue #1263: Track last text content for result summary
921
926
  let authError = false;
927
+ const baseBranchCommandIntervention = createPullRequestBaseBranchCommandIntervention({
928
+ expectedBaseBranch,
929
+ prNumber,
930
+ log,
931
+ toolLabel: 'Codex',
932
+ stopSession: async () => {
933
+ if (!execCommand?.kill) return false;
934
+ execCommand.kill('SIGTERM');
935
+ return true;
936
+ },
937
+ });
922
938
  let codexJsonState = {
923
939
  sessionId: null,
924
940
  authError: false,
@@ -949,6 +965,7 @@ export const executeCodexCommand = async params => {
949
965
  lastMessage = output;
950
966
 
951
967
  codexJsonState = parseCodexExecJsonOutput(output, codexJsonState, mappedModel);
968
+ await baseBranchCommandIntervention.handleCommandExecutions(codexJsonState.commandExecutions);
952
969
 
953
970
  if (interactiveHandler || progressMonitor) {
954
971
  for (const rawLine of output.split('\n')) {
@@ -990,6 +1007,7 @@ export const executeCodexCommand = async params => {
990
1007
  await log(errorOutput, { stream: 'stderr' });
991
1008
  }
992
1009
  codexJsonState = parseCodexExecJsonOutput(errorOutput, codexJsonState, mappedModel);
1010
+ await baseBranchCommandIntervention.handleCommandExecutions(codexJsonState.commandExecutions);
993
1011
  } else if (chunk.type === 'exit') {
994
1012
  exitCode = chunk.code;
995
1013
  }
@@ -1064,6 +1082,31 @@ export const executeCodexCommand = async params => {
1064
1082
  await log(`šŸ¤– Codex exec JSON did not expose model IDs; using requested model for reporting: ${mappedModel}`, { verbose: true });
1065
1083
  }
1066
1084
 
1085
+ const baseBranchIntervention = baseBranchCommandIntervention.getIntervention();
1086
+ if (baseBranchIntervention) {
1087
+ if ((sessionId || argv.resume) && baseBranchInterventionResumeCount < 1) {
1088
+ argv.resume = sessionId || argv.resume;
1089
+ baseBranchInterventionPrompt = baseBranchIntervention.message;
1090
+ baseBranchInterventionResumeCount++;
1091
+ await log('\nšŸ”„ Resuming Codex with requested base-branch correction prompt...');
1092
+ return await executeWithRetry();
1093
+ }
1094
+
1095
+ return {
1096
+ success: false,
1097
+ sessionId,
1098
+ limitReached,
1099
+ limitResetTime,
1100
+ codexJsonDetails: codexJsonState,
1101
+ errorInfo: {
1102
+ message: baseBranchIntervention.message,
1103
+ violation: baseBranchIntervention.violation,
1104
+ },
1105
+ result: baseBranchIntervention.message,
1106
+ resultSummary: lastTextContent || null,
1107
+ };
1108
+ }
1109
+
1067
1110
  const firstActualModelId = mappedModel;
1068
1111
  const pricingInfo = firstActualModelId ? await calculatePricing(firstActualModelId, codexJsonState.tokenUsage.stepCount > 0 ? codexJsonState.tokenUsage : null) : null;
1069
1112
  if (pricingInfo?.totalCostUSD !== null && pricingInfo?.totalCostUSD !== undefined) {
@@ -8,6 +8,7 @@ import { getHandoffSubPrompt } from './handoff.prompts.lib.mjs';
8
8
  import { getExperimentsExamplesSubPrompt } from './experiments-examples.prompts.lib.mjs';
9
9
  import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
10
10
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
11
+ import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
11
12
 
12
13
  /**
13
14
  * Build the user prompt for Codex
@@ -51,6 +52,11 @@ export const buildUserPrompt = params => {
51
52
  }
52
53
  }
53
54
 
55
+ const requestedBaseBranchDirective = buildRequestedBaseBranchDirective(argv);
56
+ if (requestedBaseBranchDirective) {
57
+ promptLines.push(requestedBaseBranchDirective);
58
+ }
59
+
54
60
  // Add blank line
55
61
  promptLines.push('');
56
62
 
@@ -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 Gemini
@@ -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) {