@link-assistant/hive-mind 2.9.1 → 2.10.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 +12 -0
- package/README.hi.md +5 -0
- package/README.md +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +5 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +15 -8
- package/src/agents-md-claude-support.lib.mjs +2 -2
- package/src/claude.lib.mjs +10 -10
- package/src/codex-capability-preflight.lib.mjs +225 -27
- package/src/codex-health.lib.mjs +123 -0
- package/src/codex.lib.mjs +58 -92
- package/src/formal-ai.lib.mjs +132 -0
- package/src/gemini.lib.mjs +11 -5
- package/src/isolation-runner.lib.mjs +57 -3
- package/src/models/index.mjs +31 -6
- package/src/opencode.lib.mjs +43 -36
- package/src/option-suggestions.lib.mjs +1 -0
- package/src/qwen.lib.mjs +10 -5
- package/src/solve.config.lib.mjs +9 -0
- package/src/solve.mjs +3 -1
- package/src/solve.validation.lib.mjs +16 -1
|
@@ -15,6 +15,7 @@ import { ensureUseM } from './use-m-bootstrap.lib.mjs';
|
|
|
15
15
|
|
|
16
16
|
import crypto from 'crypto';
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
|
+
import { lookup as lookupHost } from 'node:dns/promises';
|
|
18
19
|
import fs from 'node:fs';
|
|
19
20
|
import os from 'node:os';
|
|
20
21
|
import path from 'node:path';
|
|
@@ -53,6 +54,7 @@ const HIVE_MIND_IMAGE_REPO = 'konard/hive-mind';
|
|
|
53
54
|
const HIVE_MIND_DIND_IMAGE_REPO = 'konard/hive-mind-dind';
|
|
54
55
|
const DEFAULT_HIVE_MIND_IMAGE_TAG = 'latest';
|
|
55
56
|
const DOCKER_CONTAINER_HOME = '/home/box';
|
|
57
|
+
const FORMAL_AI_COMPOSE_HOSTNAME = 'link-assistant-formal-ai';
|
|
56
58
|
// Default path where the host Docker socket is bind-mounted inside a DinD
|
|
57
59
|
// container so box's host-image passthrough can copy host images into the
|
|
58
60
|
// nested daemon. Matches box's own DIND_HOST_DOCKER_SOCK default. The deploy
|
|
@@ -227,6 +229,44 @@ function resolveImageVariant(image, env = process.env) {
|
|
|
227
229
|
return image.includes('hive-mind-dind') ? 'dind' : env.HIVE_MIND_IMAGE_VARIANT || 'regular';
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Resolve an outer Compose HTTP service before handing its origin to a nested
|
|
234
|
+
* Docker daemon. The nested daemon has its own DNS namespace, but it can route
|
|
235
|
+
* to the outer service's address through the parent container.
|
|
236
|
+
*
|
|
237
|
+
* HTTPS names are deliberately preserved for certificate verification.
|
|
238
|
+
*/
|
|
239
|
+
export async function resolveFormalAiIsolationEnv(env = process.env, { lookup = lookupHost } = {}) {
|
|
240
|
+
const baseUrl = env.HIVE_MIND_FORMAL_AI_BASE_URL;
|
|
241
|
+
if (!baseUrl) return env;
|
|
242
|
+
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = new URL(baseUrl);
|
|
246
|
+
} catch {
|
|
247
|
+
return env;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (parsed.protocol !== 'http:' || parsed.hostname !== FORMAL_AI_COMPOSE_HOSTNAME) {
|
|
251
|
+
return env;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const addresses = await lookup(parsed.hostname, { all: true, verbatim: true });
|
|
256
|
+
const selected = addresses.find(candidate => candidate.family === 4) || addresses[0];
|
|
257
|
+
if (!selected?.address) return env;
|
|
258
|
+
|
|
259
|
+
const host = selected.family === 6 ? `[${selected.address}]` : selected.address;
|
|
260
|
+
return {
|
|
261
|
+
...env,
|
|
262
|
+
HIVE_MIND_FORMAL_AI_BASE_URL: `${parsed.protocol}//${host}${parsed.port ? `:${parsed.port}` : ''}`,
|
|
263
|
+
};
|
|
264
|
+
} catch {
|
|
265
|
+
// Keep the hostname for deployments whose nested DNS can resolve it.
|
|
266
|
+
return env;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
230
270
|
/**
|
|
231
271
|
* Build the `$` (start-command) arguments that launch a Docker-isolated task
|
|
232
272
|
* using start-command's NATIVE Docker backend (`$ --isolated docker`).
|
|
@@ -264,6 +304,13 @@ export function buildDockerIsolationStartArgs(command, args = [], options = {})
|
|
|
264
304
|
// working directory comes from the image's WORKDIR.
|
|
265
305
|
startArgs.push('-e', `HOME=${DOCKER_CONTAINER_HOME}`, '-e', `HIVE_MIND_PARENT_SESSION_ID=${sessionId || ''}`, '-e', `HIVE_MIND_IMAGE_VARIANT=${resolveImageVariant(image, env)}`);
|
|
266
306
|
|
|
307
|
+
// A persistent Formal AI server normally runs beside the Telegram/root
|
|
308
|
+
// container. Docker-isolated `/solve` jobs must receive the same endpoint;
|
|
309
|
+
// otherwise the wrapper starts a per-job server and loses shared memory.
|
|
310
|
+
if (env.HIVE_MIND_FORMAL_AI_BASE_URL) {
|
|
311
|
+
startArgs.push('-e', `HIVE_MIND_FORMAL_AI_BASE_URL=${env.HIVE_MIND_FORMAL_AI_BASE_URL}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
267
314
|
for (const mount of getDockerIsolationAuthMounts({ tool, env, homeDir, existsSync })) {
|
|
268
315
|
startArgs.push('--volume', `${mount.source}:${mount.target}`);
|
|
269
316
|
}
|
|
@@ -620,14 +667,21 @@ export async function executeWithIsolation(command, args, options = {}) {
|
|
|
620
667
|
console.log(`[VERBOSE] isolation-runner: Backend: ${backend}, Session ID: ${sessionId}`);
|
|
621
668
|
}
|
|
622
669
|
|
|
623
|
-
const
|
|
670
|
+
const effectiveOptions =
|
|
671
|
+
backend === 'docker'
|
|
672
|
+
? {
|
|
673
|
+
...options,
|
|
674
|
+
env: await resolveFormalAiIsolationEnv(options.env || process.env),
|
|
675
|
+
}
|
|
676
|
+
: options;
|
|
677
|
+
const startCommandArgs = buildStartCommandArgs(command, args, { ...effectiveOptions, sessionId });
|
|
624
678
|
|
|
625
679
|
if (verbose) {
|
|
626
680
|
console.log(`[VERBOSE] isolation-runner: ${[binPath, ...startCommandArgs].map(shellQuote).join(' ')}`);
|
|
627
681
|
if (backend === 'docker') {
|
|
628
|
-
const env =
|
|
682
|
+
const env = effectiveOptions.env || process.env;
|
|
629
683
|
const image = getDockerIsolationImage({ env });
|
|
630
|
-
const mounts = getDockerIsolationAuthMounts({ tool:
|
|
684
|
+
const mounts = getDockerIsolationAuthMounts({ tool: effectiveOptions.tool, env, homeDir: effectiveOptions.homeDir || os.homedir(), existsSync: effectiveOptions.existsSync || fs.existsSync });
|
|
631
685
|
console.log('[VERBOSE] isolation-runner: Docker isolation backend: native ($ --isolated docker)');
|
|
632
686
|
console.log(`[VERBOSE] isolation-runner: Docker isolation image: ${image}`);
|
|
633
687
|
console.log(`[VERBOSE] isolation-runner: Docker isolation privileged: ${shouldRunPrivilegedDockerIsolation(image, env)}`);
|
package/src/models/index.mjs
CHANGED
|
@@ -28,10 +28,26 @@ const execFileAsync = promisify(execFile);
|
|
|
28
28
|
|
|
29
29
|
// ─── MODEL DATA ──────────────────────────────────────────────────────────────
|
|
30
30
|
|
|
31
|
+
export const FORMAL_AI_MODEL_ALIAS = 'formal-ai';
|
|
32
|
+
export const FORMAL_AI_PROVIDER_MODEL_ID = 'formalai/formal-ai';
|
|
33
|
+
|
|
34
|
+
const formalAiNativeModelAliases = {
|
|
35
|
+
[FORMAL_AI_MODEL_ALIAS]: FORMAL_AI_MODEL_ALIAS,
|
|
36
|
+
[FORMAL_AI_PROVIDER_MODEL_ID]: FORMAL_AI_MODEL_ALIAS,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const formalAiProviderModelAliases = {
|
|
40
|
+
[FORMAL_AI_MODEL_ALIAS]: FORMAL_AI_PROVIDER_MODEL_ID,
|
|
41
|
+
[FORMAL_AI_PROVIDER_MODEL_ID]: FORMAL_AI_PROVIDER_MODEL_ID,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const isFormalAiModel = model => model === FORMAL_AI_MODEL_ALIAS || model === FORMAL_AI_PROVIDER_MODEL_ID;
|
|
45
|
+
|
|
31
46
|
// Claude models (Anthropic API)
|
|
32
47
|
// Updated for Opus 4.5/4.6/4.7/4.8/5, Sonnet 4.6/5, and Fable 5 / Mythos 5 support
|
|
33
48
|
// (Issue #1221, Issue #1238, Issue #1329, Issue #1433, Issue #1620, Issue #1832, Issue #1875, Issue #2003, Issue #2096)
|
|
34
49
|
export const claudeModels = {
|
|
50
|
+
...formalAiNativeModelAliases,
|
|
35
51
|
sonnet: 'claude-sonnet-5', // Sonnet 5 (Issue #2003)
|
|
36
52
|
opus: 'claude-opus-5', // Opus 5 (default, Issue #2096)
|
|
37
53
|
haiku: 'claude-haiku-4-5-20251001', // Haiku 4.5
|
|
@@ -72,6 +88,7 @@ export const claudeModels = {
|
|
|
72
88
|
// Issue #1543: Added qwen3.6-plus-free (former default) and nemotron-3-super-free per agent PR #234
|
|
73
89
|
// Issue #1563: qwen3.6-plus-free free promotion ended (April 2026), nemotron-3-super-free is now default per agent PR #243
|
|
74
90
|
export const agentModels = {
|
|
91
|
+
...formalAiProviderModelAliases,
|
|
75
92
|
// OpenCode Zen free models (current)
|
|
76
93
|
grok: 'opencode/grok-code',
|
|
77
94
|
'grok-code': 'opencode/grok-code',
|
|
@@ -111,6 +128,7 @@ export const agentModels = {
|
|
|
111
128
|
|
|
112
129
|
// OpenCode models (OpenCode API)
|
|
113
130
|
export const opencodeModels = {
|
|
131
|
+
...formalAiProviderModelAliases,
|
|
114
132
|
gpt4: 'openai/gpt-4',
|
|
115
133
|
gpt4o: 'openai/gpt-4o',
|
|
116
134
|
claude: 'anthropic/claude-3-5-sonnet',
|
|
@@ -124,6 +142,7 @@ export const opencodeModels = {
|
|
|
124
142
|
|
|
125
143
|
// Codex models (OpenAI API)
|
|
126
144
|
export const codexModels = {
|
|
145
|
+
...formalAiNativeModelAliases,
|
|
127
146
|
gpt5: 'gpt-5',
|
|
128
147
|
'gpt-5': 'gpt-5',
|
|
129
148
|
'gpt-5.5': 'gpt-5.5',
|
|
@@ -196,6 +215,7 @@ export const CODEX_MODEL_VARIANTS = getCodexModelVariants();
|
|
|
196
215
|
|
|
197
216
|
// Qwen Code models
|
|
198
217
|
export const qwenModels = {
|
|
218
|
+
...formalAiNativeModelAliases,
|
|
199
219
|
qwen: 'qwen3-coder-plus',
|
|
200
220
|
'qwen-coder': 'qwen3-coder-plus',
|
|
201
221
|
qwen3: 'qwen3-coder-plus',
|
|
@@ -210,6 +230,7 @@ export const qwenModels = {
|
|
|
210
230
|
// Keep aliases aligned with the Gemini CLI model aliases documented in
|
|
211
231
|
// docs/cli/cli-reference.md: auto, pro, flash, and flash-lite.
|
|
212
232
|
export const geminiModels = {
|
|
233
|
+
...formalAiNativeModelAliases,
|
|
213
234
|
auto: 'auto',
|
|
214
235
|
gemini: 'gemini-2.5-flash',
|
|
215
236
|
flash: 'gemini-2.5-flash',
|
|
@@ -492,6 +513,10 @@ export const mapModelForTool = (tool, model) => {
|
|
|
492
513
|
* @returns {boolean} True if the model is compatible with the tool
|
|
493
514
|
*/
|
|
494
515
|
export const isModelCompatibleWithTool = (tool, model) => {
|
|
516
|
+
if (isFormalAiModel(model)) {
|
|
517
|
+
return ['claude', 'agent', 'opencode', 'codex', 'qwen', 'gemini'].includes(tool);
|
|
518
|
+
}
|
|
519
|
+
|
|
495
520
|
const mappedModel = mapModelForTool(tool, model);
|
|
496
521
|
|
|
497
522
|
switch (tool) {
|
|
@@ -539,12 +564,12 @@ export const getValidModelsForTool = tool => {
|
|
|
539
564
|
// Primary (non-alias, non-deprecated) short names shown in CLI help descriptions
|
|
540
565
|
// These are the recommended model names users should see in --model help text
|
|
541
566
|
export const primaryModelNames = {
|
|
542
|
-
claude: ['opus', 'sonnet', 'haiku', 'opusplan', 'fable'],
|
|
543
|
-
opencode: ['grok', 'gpt4o'],
|
|
544
|
-
codex: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex-spark'],
|
|
545
|
-
agent: ['nemotron-3-super-free', 'minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free'],
|
|
546
|
-
qwen: ['qwen3-coder-plus', 'qwen3-coder', 'qwen3-coder-flash'],
|
|
547
|
-
gemini: ['flash', 'pro', 'flash-lite', 'auto'],
|
|
567
|
+
claude: ['opus', 'sonnet', 'haiku', 'opusplan', 'fable', FORMAL_AI_MODEL_ALIAS],
|
|
568
|
+
opencode: ['grok', 'gpt4o', FORMAL_AI_MODEL_ALIAS],
|
|
569
|
+
codex: ['gpt-5.6-sol', 'gpt-5.5', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex-spark', FORMAL_AI_MODEL_ALIAS],
|
|
570
|
+
agent: ['nemotron-3-super-free', 'minimax-m2.5-free', 'big-pickle', 'gpt-5-nano', 'glm-5-free', 'deepseek-r1-free', FORMAL_AI_MODEL_ALIAS],
|
|
571
|
+
qwen: ['qwen3-coder-plus', 'qwen3-coder', 'qwen3-coder-flash', FORMAL_AI_MODEL_ALIAS],
|
|
572
|
+
gemini: ['flash', 'pro', 'flash-lite', 'auto', FORMAL_AI_MODEL_ALIAS],
|
|
548
573
|
};
|
|
549
574
|
|
|
550
575
|
/**
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
|
|
|
20
20
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
21
21
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
22
22
|
import { opencodeModels, defaultModels } from './models/index.mjs';
|
|
23
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
23
24
|
import { checkPlaywrightMcpPackageAvailability, getOpenCodePlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
24
25
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
|
|
25
26
|
import { calculateAgentPricing } from './agent.lib.mjs';
|
|
@@ -212,34 +213,6 @@ export const executeOpenCodeCommand = async params => {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
// Create OpenCode configuration file with unrestricted permissions
|
|
216
|
-
// This allows OpenCode to access files outside the working directory without prompting
|
|
217
|
-
// Fixes issue #755: "Permission required to run: Access file outside working directory"
|
|
218
|
-
// Reference: https://opencode.ai/docs/config - see permissions documentation
|
|
219
|
-
const opencodeConfigPath = path.join(tempDir, 'opencode.json');
|
|
220
|
-
const opencodeConfig = {
|
|
221
|
-
$schema: 'https://opencode.ai/config.json',
|
|
222
|
-
permission: {
|
|
223
|
-
// All tool permissions set to 'allow' for unrestricted autonomous execution
|
|
224
|
-
// See: https://github.com/sst/opencode - permission options: "allow", "ask", "deny"
|
|
225
|
-
edit: 'allow', // File modification operations
|
|
226
|
-
bash: 'allow', // Command execution
|
|
227
|
-
webfetch: 'allow', // Web page retrieval
|
|
228
|
-
skill: 'allow', // Custom skills execution
|
|
229
|
-
doom_loop: 'allow', // Allow repeated identical tool calls (default: ask)
|
|
230
|
-
external_directory: 'allow', // File operations outside working directory (default: ask)
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
try {
|
|
234
|
-
await fs.writeFile(opencodeConfigPath, JSON.stringify(opencodeConfig, null, 2));
|
|
235
|
-
if (argv.verbose) {
|
|
236
|
-
await log(` Created OpenCode config: ${opencodeConfigPath}`, { verbose: true });
|
|
237
|
-
await log(' Permissions set: edit=allow, bash=allow, webfetch=allow, skill=allow, doom_loop=allow, external_directory=allow', { verbose: true });
|
|
238
|
-
}
|
|
239
|
-
} catch (configError) {
|
|
240
|
-
await log(`⚠️ Warning: Could not create OpenCode config file: ${configError.message}`, { level: 'warning' });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
216
|
// Take resource snapshot before execution
|
|
244
217
|
const resourcesBefore = await getResourceSnapshot();
|
|
245
218
|
await log('📈 System resources before execution:', { verbose: true });
|
|
@@ -259,6 +232,11 @@ export const executeOpenCodeCommand = async params => {
|
|
|
259
232
|
|
|
260
233
|
// Map model alias to full ID
|
|
261
234
|
const mappedModel = mapModelToId(argv.model);
|
|
235
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
236
|
+
tool: 'opencode',
|
|
237
|
+
model: argv.model,
|
|
238
|
+
toolPath: opencodePath,
|
|
239
|
+
});
|
|
262
240
|
const streamingTokenUsage = createAgentTokenUsage();
|
|
263
241
|
|
|
264
242
|
// Build opencode command arguments
|
|
@@ -280,11 +258,38 @@ export const executeOpenCodeCommand = async params => {
|
|
|
280
258
|
await fs.writeFile(promptFile, combinedPrompt);
|
|
281
259
|
|
|
282
260
|
// Build the full command - pipe the prompt file to opencode
|
|
283
|
-
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${
|
|
261
|
+
const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${toolInvocation.displayCommand} ${opencodeArgs})`;
|
|
284
262
|
|
|
285
|
-
await
|
|
286
|
-
|
|
287
|
-
|
|
263
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
264
|
+
if (preparedResult) return preparedResult;
|
|
265
|
+
|
|
266
|
+
// Create OpenCode configuration file with unrestricted permissions
|
|
267
|
+
// This allows OpenCode to access files outside the working directory without prompting
|
|
268
|
+
// Fixes issue #755: "Permission required to run: Access file outside working directory"
|
|
269
|
+
// Reference: https://opencode.ai/docs/config - see permissions documentation
|
|
270
|
+
const opencodeConfigPath = path.join(tempDir, 'opencode.json');
|
|
271
|
+
const opencodeConfig = {
|
|
272
|
+
$schema: 'https://opencode.ai/config.json',
|
|
273
|
+
permission: {
|
|
274
|
+
// All tool permissions set to 'allow' for unrestricted autonomous execution
|
|
275
|
+
// See: https://github.com/sst/opencode - permission options: "allow", "ask", "deny"
|
|
276
|
+
edit: 'allow', // File modification operations
|
|
277
|
+
bash: 'allow', // Command execution
|
|
278
|
+
webfetch: 'allow', // Web page retrieval
|
|
279
|
+
skill: 'allow', // Custom skills execution
|
|
280
|
+
doom_loop: 'allow', // Allow repeated identical tool calls (default: ask)
|
|
281
|
+
external_directory: 'allow', // File operations outside working directory (default: ask)
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
await fs.writeFile(opencodeConfigPath, JSON.stringify(opencodeConfig, null, 2));
|
|
286
|
+
if (argv.verbose) {
|
|
287
|
+
await log(` Created OpenCode config: ${opencodeConfigPath}`, { verbose: true });
|
|
288
|
+
await log(' Permissions set: edit=allow, bash=allow, webfetch=allow, skill=allow, doom_loop=allow, external_directory=allow', { verbose: true });
|
|
289
|
+
}
|
|
290
|
+
} catch (configError) {
|
|
291
|
+
await log(`⚠️ Warning: Could not create OpenCode config file: ${configError.message}`, { level: 'warning' });
|
|
292
|
+
}
|
|
288
293
|
|
|
289
294
|
const buildPricingInfo = async () => {
|
|
290
295
|
const tokenUsage = streamingTokenUsage;
|
|
@@ -298,17 +303,19 @@ export const executeOpenCodeCommand = async params => {
|
|
|
298
303
|
try {
|
|
299
304
|
// Pipe the prompt file to opencode via stdin
|
|
300
305
|
if (argv.resume) {
|
|
301
|
-
|
|
306
|
+
const commandRunner = $({
|
|
302
307
|
cwd: tempDir,
|
|
303
308
|
mirror: false,
|
|
304
309
|
env: opencodeEnv,
|
|
305
|
-
})
|
|
310
|
+
});
|
|
311
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --session ${argv.resume} --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --session ${argv.resume} --model ${mappedModel}`;
|
|
306
312
|
} else {
|
|
307
|
-
|
|
313
|
+
const commandRunner = $({
|
|
308
314
|
cwd: tempDir,
|
|
309
315
|
mirror: false,
|
|
310
316
|
env: opencodeEnv,
|
|
311
|
-
})
|
|
317
|
+
});
|
|
318
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} run --format json --model ${mappedModel}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} run --format json --model ${mappedModel}`;
|
|
312
319
|
}
|
|
313
320
|
|
|
314
321
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
package/src/qwen.lib.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { timeouts, retryLimits } from './config.lib.mjs';
|
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
20
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
21
21
|
import { qwenModels, defaultModels } from './models/index.mjs';
|
|
22
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
22
23
|
import { checkPlaywrightMcpPackageAvailability } from './playwright-mcp.lib.mjs';
|
|
23
24
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
24
25
|
import { getCumulativeContextInputTokens, getRestoredContextInputTokens, toTokenCount } from './context-fill.lib.mjs';
|
|
@@ -504,15 +505,19 @@ export const executeQwenCommand = async params => {
|
|
|
504
505
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
505
506
|
|
|
506
507
|
const mappedModel = mapModelToId(argv.model || defaultModels.qwen);
|
|
508
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
509
|
+
tool: 'qwen',
|
|
510
|
+
model: argv.model || defaultModels.qwen,
|
|
511
|
+
toolPath: qwenPath,
|
|
512
|
+
});
|
|
507
513
|
const resumeSession = argv.resume || null;
|
|
508
514
|
const resumeArgs = resumeSession ? ` --resume ${shellQuote(resumeSession)}` : '';
|
|
509
515
|
const appendSystemPromptArg = systemPrompt ? ` --append-system-prompt "$(cat ${shellQuote(systemPromptFile)})"` : '';
|
|
510
|
-
const commandScript = `cd ${shellQuote(tempDir)} && ${
|
|
511
|
-
const fullCommand = `(cd "${tempDir}" && ${
|
|
516
|
+
const commandScript = `cd ${shellQuote(tempDir)} && ${toolInvocation.displayCommand} --model ${shellQuote(mappedModel)} --output-format stream-json --yolo${resumeArgs}${appendSystemPromptArg} --prompt "$(cat ${shellQuote(promptFile)})"`;
|
|
517
|
+
const fullCommand = `(cd "${tempDir}" && ${toolInvocation.displayCommand} --model "${mappedModel}" --output-format stream-json --yolo${resumeSession ? ` --resume "${resumeSession}"` : ''}${systemPrompt ? ` --append-system-prompt "$(cat "${systemPromptFile}")"` : ''} --prompt "$(cat "${promptFile}")")`;
|
|
512
518
|
|
|
513
|
-
await
|
|
514
|
-
|
|
515
|
-
await log('');
|
|
519
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
520
|
+
if (preparedResult) return preparedResult;
|
|
516
521
|
|
|
517
522
|
try {
|
|
518
523
|
const execCommand = dollar({
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -715,6 +715,15 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
715
715
|
type: 'string',
|
|
716
716
|
description: 'Comma-separated list of MCP server names that gemini-cli is allowed to call (passes --allowed-mcp-server-names to gemini-cli). Only used when --tool gemini.',
|
|
717
717
|
},
|
|
718
|
+
// Issue #2102: the Codex capability preflight discovers requirements from the
|
|
719
|
+
// issue text and the target repository's agent instruction files (AGENTS.md,
|
|
720
|
+
// CLAUDE.md, .codex/). This is the escape hatch for a requirement no document
|
|
721
|
+
// states — codex's own `request_plugin_install` can never install anything
|
|
722
|
+
// under `codex exec`, so declaring it here is the only way in.
|
|
723
|
+
'require-codex-plugin': {
|
|
724
|
+
type: 'string',
|
|
725
|
+
description: 'Comma-separated list of Codex plugins (plugin@marketplace) that must be installed into the scoped CODEX_HOME before codex exec starts, in addition to the ones discovered from the issue text and the repository AGENTS.md/CLAUDE.md files. Fails the run when a listed plugin is unavailable. Equivalent to HIVE_MIND_CODEX_REQUIRED_PLUGINS. Only used when --tool codex.',
|
|
726
|
+
},
|
|
718
727
|
};
|
|
719
728
|
|
|
720
729
|
function hasRawOption(rawArgs, optionName) {
|
package/src/solve.mjs
CHANGED
|
@@ -253,7 +253,8 @@ if (argv.planModel) {
|
|
|
253
253
|
if (argv.subAgentModel) await validateAndExitOnInvalidClaudeSubAgentModel(argv.subAgentModel, tool, safeExit);
|
|
254
254
|
|
|
255
255
|
// Perform all system checks (skip tool connection check in dry-run or when --skip-tool-connection-check; model validation always runs)
|
|
256
|
-
const
|
|
256
|
+
const prepareOnly = argv.dryRun || argv.onlyPrepareCommand;
|
|
257
|
+
const skipToolConnectionCheck = prepareOnly || argv.skipToolConnectionCheck || argv.toolConnectionCheck === false;
|
|
257
258
|
const { cascadePlaywrightMcpDisable, ensureSolvePlaywrightMcpReady } = await import('./playwright-mcp.lib.mjs');
|
|
258
259
|
await cascadePlaywrightMcpDisable(argv, log);
|
|
259
260
|
if (!(await performSystemChecks(argv.minDiskSpace || 10240, skipToolConnectionCheck, argv.model, argv))) {
|
|
@@ -864,6 +865,7 @@ try {
|
|
|
864
865
|
await safeExit(shutdownExitCode, 'Graceful shutdown after AI working session', { skipPreExit: true });
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
if (toolResult.preparedOnly) await safeExit(0, 'Command prepared', { skipPreExit: true });
|
|
867
869
|
const { success } = toolResult;
|
|
868
870
|
sessionId = toolResult.sessionId;
|
|
869
871
|
let anthropicTotalCostUSD = toolResult.anthropicTotalCostUSD;
|
|
@@ -303,7 +303,22 @@ export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnecti
|
|
|
303
303
|
// Skip tool connection validation if in dry-run mode or explicitly requested
|
|
304
304
|
if (!skipToolConnection) {
|
|
305
305
|
let isToolConnected;
|
|
306
|
-
|
|
306
|
+
const { isFormalAiModel } = await import('./models/index.mjs');
|
|
307
|
+
if (isFormalAiModel(model)) {
|
|
308
|
+
const { validateFormalAiToolConnection } = await import('./formal-ai.lib.mjs');
|
|
309
|
+
const formalAiValidation = await validateFormalAiToolConnection(argv.tool || 'claude');
|
|
310
|
+
isToolConnected = formalAiValidation.valid;
|
|
311
|
+
if (isToolConnected) {
|
|
312
|
+
await log(`✅ Formal AI wrapper and ${argv.tool || 'claude'} CLI are available`);
|
|
313
|
+
if (formalAiValidation.version) {
|
|
314
|
+
await log(`📦 ${argv.tool || 'claude'} CLI version: ${formalAiValidation.version}`);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
await log(`❌ Formal AI dispatch validation failed: ${formalAiValidation.error}`, { level: 'error' });
|
|
318
|
+
await log(' Install or update the wrapper with: cargo install formal-ai', { level: 'error' });
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
} else if (argv.useAgentCommander) {
|
|
307
322
|
const agentCommanderLib = await import('./agent-commander.lib.mjs');
|
|
308
323
|
isToolConnected = await agentCommanderLib.validateAgentCommanderConnection({
|
|
309
324
|
tool: argv.tool || 'claude',
|