@link-assistant/hive-mind 2.12.4 → 2.13.0

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.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - aaf809a: Recognize subscription/account access blocks from every supported CLI (Claude, Codex, Qwen, Gemini, opencode) as their own error class: stop the run instead of retrying or switching model, auto-commit and push the in-flight work first, report what happened and what to do in the terminal, in the `/solve` exit message and in the Telegram completion message (en/ru/zh/hi), and stop the `/hive` queue so the fleet no longer rediscovers the block once per issue.
8
+
9
+ ## 2.12.5
10
+
11
+ ### Patch Changes
12
+
13
+ - 77ed9bc: Keep Formal AI repository requests bounded, target the discovered pull request URL, and stop retrying explicit `planned_not_executed` results as successful work.
14
+
3
15
  ## 2.12.4
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.12.4",
3
+ "version": "2.13.0",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -9,6 +9,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
10
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
11
11
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
12
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
13
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
12
14
 
13
15
  /**
14
16
  * Build the user prompt for Agent
@@ -16,6 +18,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
16
18
  * @returns {string} The formatted user prompt
17
19
  */
18
20
  export const buildUserPrompt = params => {
21
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
22
+ if (formalAiPrompt !== null) return formalAiPrompt;
23
+
19
24
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
20
25
 
21
26
  const promptLines = [];
@@ -92,6 +97,13 @@ export const buildUserPrompt = params => {
92
97
  export const buildSystemPrompt = params => {
93
98
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
94
99
 
100
+ // Issue #2158: Formal AI's deterministic intent router considers the whole
101
+ // provider request, including this caller-owned workflow prompt. Command
102
+ // examples such as `sudo` and `pwd` were consequently executed as if the
103
+ // issue requested them. The compact user prompt remains the repository
104
+ // objective; Formal AI supplies its own execution policy.
105
+ if (isFormalAiModel(argv?.model)) return '';
106
+
95
107
  // When in fork mode, screenshots are pushed to the fork, not the original repo
96
108
  const screenshotRepoPath = argv?.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
97
109
 
@@ -9,6 +9,7 @@ import { isENOSPC, buildToolErrorMessage } from './lib.mjs';
9
9
  import { reportError } from './sentry.lib.mjs';
10
10
  import { timeouts, retryLimits, claudeCode, getClaudeEnv, getMaxOutputTokensForModel } from './config.lib.mjs';
11
11
  import { detectUsageLimit, formatUsageLimitMessage, isUsageLimitError } from './usage-limit.lib.mjs';
12
+ import { detectSubscriptionError, SUBSCRIPTION_BLOCKED_MARKER } from './subscription-error.lib.mjs'; // Issue #2161
12
13
  import { createInteractiveHandler } from './interactive-mode.lib.mjs';
13
14
  import { setupBidirectionalHandler, finalizeBidirectionalHandler, validateBidirectionalModeConfig, attachStreamingInput } from './bidirectional-interactive.lib.mjs';
14
15
  import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
@@ -376,6 +377,10 @@ export const executeClaudeCommand = async params => {
376
377
  let isInternalServerError = false;
377
378
  let isRequestTimeout = false;
378
379
  let isRateLimitError = false; // Issue #1924: server-side 429 temporary rate limiting
380
+ // Issue #2161: account/subscription-level block (e.g. oauth_org_not_allowed).
381
+ // Terminal — never retried, never model-switched; carried out to the caller
382
+ // so /solve can stop with a specific diagnosis instead of a generic failure.
383
+ let subscriptionError = null;
379
384
  let apiMarkedNotRetryable = false;
380
385
  let resultNumTurns = 0;
381
386
  let stderrErrors = [];
@@ -731,6 +736,24 @@ export const executeClaudeCommand = async params => {
731
736
  isRateLimitError = true;
732
737
  await log(`⚠️ Detected server-side rate limiting (429) from Claude CLI (will retry with --resume). request_id=${data.request_id || 'unknown'}`, { verbose: true });
733
738
  }
739
+ // Issue #2161: account/subscription block. `data.error` carries the
740
+ // machine-readable code ("oauth_org_not_allowed" for the reported
741
+ // case) alongside api_error_status 403 — a far stronger signal than
742
+ // the rendered sentence, so it is passed to the detector first.
743
+ if (!subscriptionError) {
744
+ subscriptionError = detectSubscriptionError({
745
+ message: lastMessage,
746
+ tool: 'claude',
747
+ errorCode: typeof data.error === 'string' ? data.error : null,
748
+ apiErrorStatus: data.api_error_status,
749
+ terminalReason: data.terminal_reason,
750
+ });
751
+ if (subscriptionError) {
752
+ // Not verbose: this is the reason the whole run is about to end.
753
+ await log(`${SUBSCRIPTION_BLOCKED_MARKER} — ${subscriptionError.label}`);
754
+ await log(` code=${subscriptionError.code || 'n/a'} http=${data.api_error_status || 'n/a'} terminal_reason=${data.terminal_reason || 'n/a'} request_id=${data.request_id || 'unknown'}`, { verbose: true });
755
+ }
756
+ }
734
757
  // Issue #1834: Detect corrupted extended-thinking-block 400 (un-resumable session).
735
758
  // Capture diagnostics (request id, content path) to aid debugging and upstream reports.
736
759
  if ((lastMessage.includes('thinking') || lastMessage.includes('redacted_thinking')) && lastMessage.includes('cannot be modified')) {
@@ -765,6 +788,26 @@ export const executeClaudeCommand = async params => {
765
788
  await log(`🤖 Sub-agent "${callEntry.description || 'unknown'}" completed: ${data.usage.total_tokens} total tokens`, { verbose: true });
766
789
  }
767
790
  }
791
+ // Issue #2161: Claude Code injects API failures as synthetic assistant
792
+ // messages flagged `is_api_error_message` and carrying the error code.
793
+ // In the reported run this arrived ~40s before the terminal result
794
+ // event, so detecting it here surfaces the diagnosis earlier.
795
+ if (data.type === 'assistant' && data.is_api_error_message === true && !subscriptionError) {
796
+ const apiErrorText = getClaudeMessageContent(data)
797
+ .filter(item => item.type === 'text' && item.text)
798
+ .map(item => item.text)
799
+ .join('\n');
800
+ subscriptionError = detectSubscriptionError({
801
+ message: apiErrorText,
802
+ tool: 'claude',
803
+ errorCode: typeof data.error === 'string' ? data.error : null,
804
+ });
805
+ if (subscriptionError) {
806
+ if (apiErrorText) lastMessage = apiErrorText;
807
+ await log(`${SUBSCRIPTION_BLOCKED_MARKER} — ${subscriptionError.label}`);
808
+ await log(` code=${subscriptionError.code || 'n/a'} request_id=${data.request_id || 'unknown'} uuid=${data.uuid || 'unknown'}`, { verbose: true });
809
+ }
810
+ }
768
811
  if (data.type === 'assistant' && data.message && data.message.content) {
769
812
  const content = getClaudeMessageContent(data);
770
813
  for (const item of content) {
@@ -977,7 +1020,12 @@ export const executeClaudeCommand = async params => {
977
1020
  }
978
1021
  // Issues #1331, #1353, #1472/#1475: Unified transient error retry (exponential backoff, session preservation)
979
1022
  const isTransientError = isStartupTimeout || isActivityTimeout || isOverloadError || isInternalServerError || is503Error || isRequestTimeout || isRateLimitError || retryableLastError.isRetryable || (lastMessage.includes('API Error: 500') && (lastMessage.includes('Overloaded') || lastMessage.includes('Internal server error'))) || (lastMessage.includes('API Error: 529') && (lastMessage.includes('overloaded_error') || lastMessage.includes('Overloaded'))) || (lastMessage.includes('api_error') && lastMessage.includes('Overloaded')) || (lastMessage.includes('overloaded_error') && lastMessage.includes('Overloaded')) || lastMessage.includes('API Error: 503') || (lastMessage.includes('503') && (lastMessage.includes('upstream connect error') || lastMessage.includes('remote connection failure'))) || lastMessage === 'Request timed out' || lastMessage.includes('Request timed out');
980
- if ((commandFailed || isTransientError) && isTransientError) {
1023
+ // Issue #2161: an account/subscription block short-circuits every retry
1024
+ // path. Stale transient flags from earlier in the run (an overload at hour
1025
+ // one, say) must not schedule a retry that is guaranteed to fail the same
1026
+ // way — and each retry would burn another full startup against a provider
1027
+ // that has already refused the credentials.
1028
+ if ((commandFailed || isTransientError) && isTransientError && !subscriptionError) {
981
1029
  // Issue #1472/#1475: Startup/activity timeout → 30s–2min backoff; #1353: Request timeout → 5min–1hr; general → 2min–30min
982
1030
  const isTimeoutRetry = isStartupTimeout || isActivityTimeout;
983
1031
  const maxRetries = isTimeoutRetry ? retryLimits.maxTransientErrorRetries : isRequestTimeout ? retryLimits.maxRequestTimeoutRetries : retryLimits.maxTransientErrorRetries;
@@ -1005,6 +1053,7 @@ export const executeClaudeCommand = async params => {
1005
1053
  resultSummary,
1006
1054
  // Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
1007
1055
  errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: 'API explicitly marked error as not retryable', toolLabel: 'Claude' }), exitCode },
1056
+ subscriptionError, // Issue #2161
1008
1057
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1009
1058
  };
1010
1059
  }
@@ -1055,6 +1104,7 @@ export const executeClaudeCommand = async params => {
1055
1104
  resultSummary, // Issue #1263: Include result summary
1056
1105
  // Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
1057
1106
  errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: `Transient API error persisted after ${maxRetries} retries`, toolLabel: 'Claude' }), exitCode },
1107
+ subscriptionError, // Issue #2161
1058
1108
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1059
1109
  };
1060
1110
  }
@@ -1123,6 +1173,7 @@ export const executeClaudeCommand = async params => {
1123
1173
  // Issue #1845: surface the core error (e.g. "API Error: Output blocked by content filtering policy").
1124
1174
  // Issue #1941: a lone "}" fragment at interrupt time must not become "CLAUDE execution failed with }".
1125
1175
  errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: `Claude command failed with exit code ${exitCode}`, toolLabel: 'Claude' }), exitCode },
1176
+ subscriptionError, // Issue #2161: terminal account block — /solve stops and preserves the work
1126
1177
  queuedFeedback, // Issue #817: Bidirectional mode feedback
1127
1178
  };
1128
1179
  }
@@ -11,6 +11,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
11
11
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
12
12
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
13
13
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
14
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
15
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
14
16
 
15
17
  /**
16
18
  * Build the user prompt for Claude
@@ -18,6 +20,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
18
20
  * @returns {string} The formatted user prompt
19
21
  */
20
22
  export const buildUserPrompt = params => {
23
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
24
+ if (formalAiPrompt !== null) return formalAiPrompt;
25
+
21
26
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, owner, repo, argv, contributingGuidelines, claudeVersion } = params;
22
27
 
23
28
  if (argv?.minimalRestartContext && argv.resume) {
@@ -105,6 +110,10 @@ export const buildUserPrompt = params => {
105
110
  export const buildSystemPrompt = params => {
106
111
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
107
112
 
113
+ // Issue #2158: keep caller workflow instructions out of Formal AI's task
114
+ // classifier. Formal AI provides its own execution policy.
115
+ if (isFormalAiModel(argv?.model)) return '';
116
+
108
117
  if (argv?.minimalRestartContext && argv.resume) {
109
118
  return '';
110
119
  }
@@ -10,6 +10,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
10
10
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
11
11
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
12
12
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
13
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
14
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
13
15
 
14
16
  /**
15
17
  * Build the user prompt for Codex
@@ -17,6 +19,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
17
19
  * @returns {string} The formatted user prompt
18
20
  */
19
21
  export const buildUserPrompt = params => {
22
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
23
+ if (formalAiPrompt !== null) return formalAiPrompt;
24
+
20
25
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
21
26
 
22
27
  const promptLines = [];
@@ -93,6 +98,10 @@ export const buildUserPrompt = params => {
93
98
  export const buildSystemPrompt = params => {
94
99
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
95
100
 
101
+ // Issue #2158: keep caller workflow instructions out of Formal AI's task
102
+ // classifier. Formal AI provides its own execution policy.
103
+ if (isFormalAiModel(argv?.model)) return '';
104
+
96
105
  // When in fork mode, screenshots are pushed to the fork, not the original repo
97
106
  const screenshotRepoPath = argv?.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
98
107
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Build the small repository objective sent through native CLIs to Formal AI.
3
+ *
4
+ * Formal AI owns the agent policy for its model. Repeating Hive Mind's native
5
+ * provider policy in the request both wastes context and exposes incidental
6
+ * shell-language cues to Formal AI's deterministic intent router (#2158).
7
+ */
8
+
9
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
10
+
11
+ export const buildFormalAiRepositoryPrompt = params => {
12
+ if (!isFormalAiModel(params?.argv?.model)) return null;
13
+
14
+ const { issueUrl, issueNumber, prNumber, prUrl, branchName, isContinueMode, feedbackLines, owner, repo } = params;
15
+ const issueReference = isContinueMode && issueNumber && owner && repo ? `https://github.com/${owner}/${repo}/issues/${issueNumber}` : issueUrl || `the issue linked to pull request ${prNumber}`;
16
+ const lines = [`Resolve the GitHub issue at ${issueReference} in this repository.`];
17
+
18
+ if (branchName) lines.push(`Keep the solution on branch ${branchName}.`);
19
+ if (prUrl) lines.push(`Update the pull request at ${prUrl}.`);
20
+ // The review text is caller-controlled and can contain command snippets.
21
+ // Point Formal AI to the canonical PR instead of copying those snippets into
22
+ // the intent-classification request.
23
+ if (feedbackLines?.length && prUrl) lines.push('Review and address all feedback recorded on that pull request.');
24
+
25
+ lines.push('', 'Implement and verify the solution before reporting completion.', isContinueMode ? 'Continue.' : 'Proceed.');
26
+ return `${lines.join('\n')}\n`;
27
+ };
28
+
29
+ export default { buildFormalAiRepositoryPrompt };
@@ -217,6 +217,36 @@ export const createPreparedToolResult = preparedCommand => ({
217
217
  errorDuringExecution: false,
218
218
  });
219
219
 
220
+ const FORMAL_AI_NON_EXECUTION_PATTERNS = [/^\s*planned,\s*not executed\b/im, /^\s*planned_not_executed\s*$/im, /^\s*terminal_state\s+["']?planned_not_executed\b/im];
221
+
222
+ /**
223
+ * Turn Formal AI's explicit non-execution terminal state into a failed tool
224
+ * result (issue #2158).
225
+ *
226
+ * Formal AI 0.339.1 truthfully reports repository work as
227
+ * `planned_not_executed`, but each native CLI exits zero. Treating that process
228
+ * exit as a successful solve made `--auto-restart-until-mergeable` repeat the
229
+ * same deterministic plan five times. Keep the model's summary for evidence,
230
+ * while giving Hive Mind an actionable terminal failure.
231
+ */
232
+ export const classifyFormalAiToolResult = ({ model, toolResult } = {}) => {
233
+ if (!toolResult || !isFormalAiModel(model) || toolResult.success === false) return toolResult;
234
+
235
+ const evidence = [toolResult.resultSummary, toolResult.result, toolResult.lastMessage, toolResult.output].filter(value => typeof value === 'string').join('\n');
236
+ if (!FORMAL_AI_NON_EXECUTION_PATTERNS.some(pattern => pattern.test(evidence))) return toolResult;
237
+
238
+ return {
239
+ ...toolResult,
240
+ success: false,
241
+ errorDuringExecution: true,
242
+ formalAiNonExecution: true,
243
+ errorInfo: {
244
+ code: 'FORMAL_AI_PLANNED_NOT_EXECUTED',
245
+ message: "Formal AI did not execute repository work; it returned the terminal state planned_not_executed. Fix or upgrade Formal AI's repository-work executor before retrying.",
246
+ },
247
+ };
248
+ };
249
+
220
250
  export const logPreparedToolCommand = async ({ argv, fullCommand, log, formatAligned }) => {
221
251
  await log(`\n${formatAligned('📝', 'Raw command:', '')}`);
222
252
  await log(fullCommand);
@@ -9,6 +9,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
10
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
11
11
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
12
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
13
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
12
14
 
13
15
  /**
14
16
  * Build the user prompt for Gemini
@@ -16,6 +18,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
16
18
  * @returns {string} The formatted user prompt
17
19
  */
18
20
  export const buildUserPrompt = params => {
21
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
22
+ if (formalAiPrompt !== null) return formalAiPrompt;
23
+
19
24
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
20
25
 
21
26
  const promptLines = [];
@@ -81,6 +86,10 @@ export const buildUserPrompt = params => {
81
86
  export const buildSystemPrompt = params => {
82
87
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
83
88
 
89
+ // Issue #2158: keep caller workflow instructions out of Formal AI's task
90
+ // classifier. Formal AI provides its own execution policy.
91
+ if (isFormalAiModel(argv?.model)) return '';
92
+
84
93
  const screenshotRepoPath = argv?.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
85
94
 
86
95
  let workspaceInstructions = '';
@@ -241,6 +241,14 @@ export function normalizeGitHubUrl(url) {
241
241
  const parsed = parseGitHubUrl(url);
242
242
  return parsed.valid ? parsed.normalized : null;
243
243
  }
244
+
245
+ /** Build the canonical web URL for a pull request already identified by GitHub. */
246
+ export function buildGitHubPullRequestUrl({ owner, repo, number } = {}) {
247
+ if (!owner || !repo || !Number.isInteger(Number(number)) || Number(number) <= 0) {
248
+ throw new TypeError('A GitHub pull request URL requires owner, repo, and a positive integer number');
249
+ }
250
+ return `https://github.com/${owner}/${repo}/pull/${Number(number)}`;
251
+ }
244
252
  /**
245
253
  * Check if a URL is a valid GitHub URL of a specific type
246
254
  * @param {string} url - The URL to check
@@ -20,8 +20,8 @@ export { buildCostInfoString };
20
20
  // #1756: route gh exec calls through transient + rate-limit retry wrapper
21
21
  import { execGhWithRetry } from './github-rate-limit.lib.mjs';
22
22
  import { QUIET_PROBE } from './quiet-probe.lib.mjs'; // issues #2130, #2135: keep read-only probe payloads out of the attached log
23
- import { isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl } from './github-url-parser.lib.mjs';
24
- export { isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl };
23
+ import { buildGitHubPullRequestUrl, isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl } from './github-url-parser.lib.mjs';
24
+ export { buildGitHubPullRequestUrl, isGitHubUrlType, normalizeGitHubUrl, parseGitHubUrl };
25
25
  // Issue #1625: Named marker constants (single source of truth) + in-memory tracking for tool-posted comments. See tool-comments.lib.mjs for design.
26
26
  import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
27
27
  export const maskGitHubToken = maskToken; // Alias for backward compatibility
@@ -1168,6 +1168,7 @@ export default {
1168
1168
  fetchProjectIssues,
1169
1169
  isRateLimitError,
1170
1170
  batchCheckPullRequestsForIssues,
1171
+ buildGitHubPullRequestUrl,
1171
1172
  parseGitHubUrl,
1172
1173
  normalizeGitHubUrl,
1173
1174
  isGitHubUrlType,
package/src/hive.mjs CHANGED
@@ -36,6 +36,7 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
36
36
  }
37
37
  export { createYargsConfig } from './hive.config.lib.mjs';
38
38
  import { attachChildExitHandlers } from './child-exit.lib.mjs';
39
+ import { SUBSCRIPTION_BLOCKED_MARKER } from './subscription-error.lib.mjs'; // Issue #2161
39
40
  import { isDirectExecution, withTimeout } from './hive.bootstrap.lib.mjs';
40
41
  import { createShutdownManager } from './hive.shutdown.lib.mjs';
41
42
  const isRunningDirectly = isDirectExecution(process.argv[1], import.meta.url);
@@ -653,6 +654,22 @@ if (isRunningDirectly) {
653
654
  // controlled SIGTERM to each (they run in their own detached process group, so the
654
655
  // terminal's SIGINT never reaches them); a *second* interrupt force-kills the groups.
655
656
  const activeSolveChildren = new Set();
657
+ // Issue #2161: an account/subscription block is hive-wide, not per-issue. The
658
+ // credentials every worker shares have been refused, so each remaining issue
659
+ // would spin up a full solve run only to die the same way — burning clones,
660
+ // containers and PR comments while the queue drains into "failed". The first
661
+ // worker to see the marker in its child's output records it here and stops the
662
+ // queue; the rest exit as soon as their current child returns.
663
+ let subscriptionBlock = null;
664
+ const noteSubscriptionBlock = (workerId, line) => {
665
+ if (subscriptionBlock) return;
666
+ subscriptionBlock = { workerId, line: line.trim() };
667
+ log(`\n${SUBSCRIPTION_BLOCKED_MARKER} — worker ${workerId} reported that the account can no longer use the tool:`, { level: 'error' }).catch(() => {});
668
+ log(` ${subscriptionBlock.line}`, { level: 'error' }).catch(() => {});
669
+ log(' Stopping the hive: every remaining issue would fail the same way until access is restored.', { level: 'error' }).catch(() => {});
670
+ log(' In-flight workers finish (and auto-commit their work) before the run ends.', { level: 'error' }).catch(() => {});
671
+ issueQueue.stop();
672
+ };
656
673
  // Worker function to process issues from queue
657
674
  async function worker(workerId) {
658
675
  await log(`🔧 Worker ${workerId} started`, { verbose: true });
@@ -758,6 +775,9 @@ if (isRunningDirectly) {
758
775
  const lines = data.toString().split('\n');
759
776
  for (const line of lines) {
760
777
  if (line.trim()) {
778
+ // Issue #2161: solve prints SUBSCRIPTION_BLOCKED_MARKER on a terminal
779
+ // account block. Seen here, it stops the whole hive (see noteSubscriptionBlock).
780
+ if (line.includes(SUBSCRIPTION_BLOCKED_MARKER)) noteSubscriptionBlock(workerId, line);
761
781
  log(` [${solveCommand} worker-${workerId}] ${line}`).catch(logError => {
762
782
  reportError(logError, {
763
783
  context: 'worker_stdout_log',
@@ -777,6 +797,7 @@ if (isRunningDirectly) {
777
797
  const lines = data.toString().split('\n');
778
798
  for (const line of lines) {
779
799
  if (line.trim()) {
800
+ if (line.includes(SUBSCRIPTION_BLOCKED_MARKER)) noteSubscriptionBlock(workerId, line); // Issue #2161
780
801
  log(` [${solveCommand} worker-${workerId} stderr] ${line}`).catch(logError => {
781
802
  reportError(logError, {
782
803
  context: 'worker_stderr_log',
@@ -813,6 +834,14 @@ if (isRunningDirectly) {
813
834
  await log(` 🛑 Worker ${workerId} stopped gracefully during shutdown on ${issueUrl} (exit ${exitCode}, ${duration}s)`);
814
835
  gracefulStop = true;
815
836
  break; // stop processing more PRs for this issue
837
+ } else if (subscriptionBlock) {
838
+ // Issue #2161: the run did not fail because of this issue — the account
839
+ // lost access mid-flight. Report the real reason and stop; solve has
840
+ // already auto-committed whatever work existed.
841
+ await log(` ${SUBSCRIPTION_BLOCKED_MARKER} Worker ${workerId} stopped on ${issueUrl} after ${duration}s: the tool account can no longer be used (exit ${exitCode}).`, { level: 'error' });
842
+ await log(` Restore access, then re-run the hive — this issue stays queued, not failed.`, { level: 'error' });
843
+ gracefulStop = true;
844
+ break;
816
845
  } else {
817
846
  throw new Error(`${solveCommand} exited with code ${exitCode}`);
818
847
  }
@@ -1260,6 +1289,16 @@ if (isRunningDirectly) {
1260
1289
  }
1261
1290
  await log('\n👋 Hive Mind monitoring stopped');
1262
1291
  await log(` 📁 Full log file: ${absoluteLogPath}`);
1292
+ // Issue #2161: the hive did not simply "finish" — it was cut short because the
1293
+ // account lost access. Say so last (that is what a human scrolls to) and exit
1294
+ // non-zero so supervisors and the Telegram monitor report a failure, not a
1295
+ // clean completion.
1296
+ if (subscriptionBlock) {
1297
+ await log(`\n${SUBSCRIPTION_BLOCKED_MARKER} Hive stopped early: the tool account can no longer be used.`, { level: 'error' });
1298
+ await log(` Reported by worker ${subscriptionBlock.workerId}: ${subscriptionBlock.line}`, { level: 'error' });
1299
+ await log(' Restore subscription/account access, then start the hive again.', { level: 'error' });
1300
+ await safeExit(1, 'Subscription/account access blocked');
1301
+ }
1263
1302
  }
1264
1303
  // Issue #1823: Graceful-shutdown + force-kill logic lives in hive.shutdown.lib.mjs.
1265
1304
  // gracefulShutdown waits (uncapped) for in-flight solve workers to finish on the first
@@ -85,6 +85,14 @@ const ENGLISH_LIMITS = {
85
85
  subscription_detail_trial_ends: 'trial ends {{time}}',
86
86
  subscription_detail_trial_ends_in: 'trial ends in {{duration}}; {{time}}',
87
87
  subscription_status: 'Subscription: {{status}}',
88
+ subscription_blocked_title: 'Subscription/account access blocked',
89
+ subscription_blocked_provider: 'Provider said',
90
+ subscription_blocked_code: 'Error code',
91
+ subscription_blocked_reason: 'Why the run stopped',
92
+ subscription_blocked_note: 'This is not a usage limit: waiting, retrying or switching model cannot fix it.',
93
+ subscription_blocked_steps: 'What to do',
94
+ subscription_blocked_preserved: 'Uncommitted work was auto-committed before stopping.',
95
+ subscription_blocked_resume: 'Resume after access is restored',
88
96
  telegram_api: 'Telegram Bot API',
89
97
  telegram_flood_control: 'flood control',
90
98
  telegram_last_rate_limit: 'Last 429: {{method}}',
@@ -322,6 +322,15 @@ en
322
322
  session "session"
323
323
  start "Start"
324
324
  subscription
325
+ blocked
326
+ title "Subscription/account access blocked"
327
+ provider "Provider said"
328
+ code "Error code"
329
+ reason "Why the run stopped"
330
+ note "This is not a usage limit: waiting, retrying or switching model cannot fix it."
331
+ steps "What to do"
332
+ preserved "Uncommitted work was auto-committed before stopping."
333
+ resume "Resume after access is restored"
325
334
  detail
326
335
  ends
327
336
  label "ends {{time}}"
@@ -322,6 +322,15 @@ hi
322
322
  session "सत्र"
323
323
  start "शुरुआत"
324
324
  subscription
325
+ blocked
326
+ title "सदस्यता/खाता पहुँच अवरुद्ध"
327
+ provider "प्रदाता ने कहा"
328
+ code "त्रुटि कोड"
329
+ reason "रन क्यों रुका"
330
+ note "यह उपयोग सीमा नहीं है: प्रतीक्षा, पुनः प्रयास या मॉडल बदलना इसे ठीक नहीं करेगा।"
331
+ steps "क्या करें"
332
+ preserved "रुकने से पहले बिना कमिट किए बदलाव स्वतः कमिट कर दिए गए।"
333
+ resume "पहुँच बहाल होने पर फिर से शुरू करें"
325
334
  detail
326
335
  ends
327
336
  label "{{time}} को समाप्त होगी"
@@ -322,6 +322,15 @@ ru
322
322
  session "сеанс"
323
323
  start "Начало"
324
324
  subscription
325
+ blocked
326
+ title "Доступ по подписке/аккаунту заблокирован"
327
+ provider "Ответ провайдера"
328
+ code "Код ошибки"
329
+ reason "Почему запуск остановлен"
330
+ note "Это не лимит использования: ожидание, повтор или смена модели не помогут."
331
+ steps "Что делать"
332
+ preserved "Незакоммиченные изменения были автоматически закоммичены перед остановкой."
333
+ resume "Продолжить после восстановления доступа"
325
334
  detail
326
335
  ends
327
336
  label "заканчивается {{time}}"
@@ -322,6 +322,15 @@ zh
322
322
  session "会话"
323
323
  start "开始"
324
324
  subscription
325
+ blocked
326
+ title "订阅/账号访问被阻止"
327
+ provider "服务方提示"
328
+ code "错误代码"
329
+ reason "运行停止的原因"
330
+ note "这不是用量限制:等待、重试或切换模型都无法解决。"
331
+ steps "如何处理"
332
+ preserved "停止前已自动提交未提交的改动。"
333
+ resume "恢复访问后继续"
325
334
  detail
326
335
  ends
327
336
  label "结束于 {{time}}"
@@ -9,6 +9,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
10
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
11
11
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
12
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
13
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
12
14
 
13
15
  /**
14
16
  * Build the user prompt for OpenCode
@@ -16,6 +18,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
16
18
  * @returns {string} The formatted user prompt
17
19
  */
18
20
  export const buildUserPrompt = params => {
21
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
22
+ if (formalAiPrompt !== null) return formalAiPrompt;
23
+
19
24
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
20
25
 
21
26
  const promptLines = [];
@@ -92,6 +97,10 @@ export const buildUserPrompt = params => {
92
97
  export const buildSystemPrompt = params => {
93
98
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
94
99
 
100
+ // Issue #2158: keep caller workflow instructions out of Formal AI's task
101
+ // classifier. Formal AI provides its own execution policy.
102
+ if (isFormalAiModel(argv?.model)) return '';
103
+
95
104
  // When in fork mode, screenshots are pushed to the fork, not the original repo
96
105
  const screenshotRepoPath = argv?.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
97
106
 
@@ -9,6 +9,8 @@ import { getThinkingPromptInstruction } from './thinking-prompt.lib.mjs';
9
9
  import { buildWorkLanguageDirective } from './work-language.prompts.lib.mjs';
10
10
  import { buildRequestedBaseBranchDirective } from './solve-option-contract.prompts.lib.mjs';
11
11
  import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
12
+ import { buildFormalAiRepositoryPrompt } from './formal-ai-prompt.lib.mjs';
13
+ import { isFormalAiModel } from './formal-ai-model.lib.mjs';
12
14
 
13
15
  /**
14
16
  * Build the user prompt for Qwen Code
@@ -16,6 +18,9 @@ import { buildIssueResearchPrompt } from './deep-analysis.lib.mjs';
16
18
  * @returns {string} The formatted user prompt
17
19
  */
18
20
  export const buildUserPrompt = params => {
21
+ const formalAiPrompt = buildFormalAiRepositoryPrompt(params);
22
+ if (formalAiPrompt !== null) return formalAiPrompt;
23
+
19
24
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, tool = 'qwen' } = params;
20
25
 
21
26
  const promptLines = [];
@@ -81,6 +86,10 @@ export const buildUserPrompt = params => {
81
86
  export const buildSystemPrompt = params => {
82
87
  const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv, modelSupportsVision, forkedRepo } = params;
83
88
 
89
+ // Issue #2158: keep caller workflow instructions out of Formal AI's task
90
+ // classifier. Formal AI provides its own execution policy.
91
+ if (isFormalAiModel(argv?.model)) return '';
92
+
84
93
  const screenshotRepoPath = argv?.fork && forkedRepo ? forkedRepo : `${owner}/${repo}`;
85
94
 
86
95
  let workspaceInstructions = '';