@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a1a1f63: Allow `--model formal-ai` with every supported agentic CLI, route execution through `formal-ai with`, and provide a persistent Formal AI Docker service.
|
|
8
|
+
|
|
9
|
+
## 2.9.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a6d14db: Read the target repository's own agent instructions when deciding which Codex plugins and skills to provision. The capability preflight previously built its requirement corpus from the GitHub issue alone, so a repository that mandates a plugin in `AGENTS.md` — where the convention says to put it — provisioned nothing, and the model's own `request_plugin_install` attempt was rejected by Codex as an unrecognized `plugin_id`. Root and nested `AGENTS.md`, `CLAUDE.md` and `.codex/*.md` files under the checkout now feed the detector through a bounded walk that reports what it skipped, the zero-requirement path logs the sources it scanned instead of staying silent, a runtime plugin-install rejection is recognized and fails the run with a named diagnostic when the session produced no file changes, and `--require-codex-plugin` / `HIVE_MIND_CODEX_REQUIRED_PLUGINS` can state a requirement that no document spells out.
|
|
14
|
+
|
|
3
15
|
## 2.9.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -502,6 +502,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
502
502
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
503
503
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
504
504
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
505
|
+
|
|
506
|
+
# किसी भी समर्थित agentic CLI को Formal AI के माध्यम से route करें
|
|
507
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
508
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
509
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
505
510
|
```
|
|
506
511
|
|
|
507
512
|
> **📖 मुफ्त मॉडल गाइड**: OpenCode Zen और Kilo Gateway प्रदाताओं सहित सभी मुफ्त मॉडलों के बारे में व्यापक जानकारी के लिए [docs/FREE_MODELS.hi.md](./docs/FREE_MODELS.hi.md) देखें।
|
package/README.md
CHANGED
|
@@ -506,6 +506,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
506
506
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
507
507
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
508
508
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
509
|
+
|
|
510
|
+
# Route any supported agentic CLI through Formal AI
|
|
511
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
512
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
513
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
509
514
|
```
|
|
510
515
|
|
|
511
516
|
Current tool defaults in Hive Mind:
|
package/README.ru.md
CHANGED
|
@@ -503,6 +503,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
503
503
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
504
504
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
505
505
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
506
|
+
|
|
507
|
+
# Маршрутизация любого поддерживаемого агентного CLI через Formal AI
|
|
508
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
509
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
510
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
506
511
|
```
|
|
507
512
|
|
|
508
513
|
> **📖 Руководство по бесплатным моделям**: см. [docs/FREE_MODELS.ru.md](./docs/FREE_MODELS.ru.md) для получения полной информации обо всех бесплатных моделях, включая провайдеры OpenCode Zen и Kilo Gateway.
|
package/README.zh.md
CHANGED
|
@@ -499,6 +499,11 @@ Free Models via Kilo Gateway (with --tool agent):
|
|
|
499
499
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-5-free
|
|
500
500
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/glm-4.5-air-free
|
|
501
501
|
/solve https://github.com/owner/repo/issues/123 --tool agent --model kilo/deepseek-r1-free
|
|
502
|
+
|
|
503
|
+
# 通过 Formal AI 路由任意受支持的 Agent CLI
|
|
504
|
+
/solve https://github.com/owner/repo/issues/123 --tool agent --model formal-ai
|
|
505
|
+
/solve https://github.com/owner/repo/issues/123 --tool codex --model formal-ai
|
|
506
|
+
/solve https://github.com/owner/repo/issues/123 --tool claude --model formal-ai
|
|
502
507
|
```
|
|
503
508
|
|
|
504
509
|
> **📖 免费模型指南**:有关所有免费模型(包括 OpenCode Zen 和 Kilo Gateway 提供商)的全面信息,请参见 [docs/FREE_MODELS.zh.md](./docs/FREE_MODELS.zh.md)。
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
22
22
|
import Decimal from 'decimal.js-light';
|
|
23
23
|
import semver from 'semver';
|
|
24
24
|
import { agentModels, defaultModels, freeToBaseModelMap } from './models/index.mjs';
|
|
25
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
25
26
|
import { checkPlaywrightMcpPackageAvailability, getAgentPlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
26
27
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage } from './agent-token-usage.lib.mjs';
|
|
27
28
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
@@ -502,6 +503,11 @@ export const executeAgentCommand = async params => {
|
|
|
502
503
|
|
|
503
504
|
// Map model alias to full ID
|
|
504
505
|
const mappedModel = mapModelToId(argv.model);
|
|
506
|
+
const toolInvocation = resolveFormalAiToolInvocation({
|
|
507
|
+
tool: 'agent',
|
|
508
|
+
model: argv.model,
|
|
509
|
+
toolPath: agentPath,
|
|
510
|
+
});
|
|
505
511
|
|
|
506
512
|
// Build agent command arguments
|
|
507
513
|
let agentArgs = `--model ${mappedModel}`;
|
|
@@ -546,30 +552,31 @@ export const executeAgentCommand = async params => {
|
|
|
546
552
|
await fs.writeFile(promptFile, combinedPrompt);
|
|
547
553
|
}
|
|
548
554
|
|
|
549
|
-
const fullCommand = streamingInput ? `(cd "${tempDir}" && ${
|
|
555
|
+
const fullCommand = streamingInput ? `(cd "${tempDir}" && ${toolInvocation.displayCommand} ${agentArgs})` : `(cd "${tempDir}" && cat "${promptFile}" | ${toolInvocation.displayCommand} ${agentArgs})`;
|
|
550
556
|
|
|
551
|
-
await
|
|
552
|
-
|
|
553
|
-
await log('');
|
|
557
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
558
|
+
if (preparedResult) return preparedResult;
|
|
554
559
|
|
|
555
560
|
if (streamingInput) {
|
|
556
|
-
|
|
561
|
+
const commandRunner = $({
|
|
557
562
|
cwd: tempDir,
|
|
558
563
|
stdin: 'pipe',
|
|
559
564
|
mirror: false,
|
|
560
565
|
env: agentEnv,
|
|
561
|
-
})
|
|
566
|
+
});
|
|
567
|
+
execCommand = toolInvocation.formalAi ? commandRunner`${toolInvocation.command} ${toolInvocation.args} ${agentArgs}` : commandRunner`${toolInvocation.command} ${agentArgs}`;
|
|
562
568
|
const attached = await attachStreamingInput(bidirectionalHandler, execCommand, combinedPrompt, log, !!argv.verbose, { toolLabel: 'Agent' });
|
|
563
569
|
if (!attached) {
|
|
564
570
|
throw new Error('Agent live stream-json input requested, but stdin attachment failed');
|
|
565
571
|
}
|
|
566
572
|
} else {
|
|
567
573
|
// Pipe the prompt file to agent via stdin for the legacy one-shot path.
|
|
568
|
-
|
|
574
|
+
const commandRunner = $({
|
|
569
575
|
cwd: tempDir,
|
|
570
576
|
mirror: false,
|
|
571
577
|
env: agentEnv,
|
|
572
|
-
})
|
|
578
|
+
});
|
|
579
|
+
execCommand = toolInvocation.formalAi ? commandRunner`cat ${promptFile} | ${toolInvocation.command} ${toolInvocation.args} ${agentArgs}` : commandRunner`cat ${promptFile} | ${toolInvocation.command} ${agentArgs}`;
|
|
573
580
|
}
|
|
574
581
|
|
|
575
582
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const AGENTS_MD_FILENAMES = Object.freeze(['AGENTS.md', 'agents.md']);
|
|
2
|
-
const CLAUDE_MD_FILENAME = 'CLAUDE.md';
|
|
1
|
+
export const AGENTS_MD_FILENAMES = Object.freeze(['AGENTS.md', 'agents.md']);
|
|
2
|
+
export const CLAUDE_MD_FILENAME = 'CLAUDE.md';
|
|
3
3
|
|
|
4
4
|
const noopLog = async () => {};
|
|
5
5
|
const fallbackFormatAligned = (_icon, label, value) => `${label} ${value}`;
|
package/src/claude.lib.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { buildSolveResumeCommand } from './solve.resume-command.lib.mjs'; // Iss
|
|
|
22
22
|
import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments.lib.mjs'; // Issue #1625
|
|
23
23
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
24
24
|
import { CLAUDE_MODELS as availableModels, mapClaudeSubAgentModelToEnvValue } from './models/index.mjs'; // Issue #1221, #1978
|
|
25
|
+
import { logPreparedToolCommand, resolveFormalAiToolInvocation } from './formal-ai.lib.mjs';
|
|
25
26
|
import { buildMcpConfigWithoutPlaywright, ensureClaudePlaywrightMcpServer } from './playwright-mcp.lib.mjs';
|
|
26
27
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
27
28
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
@@ -614,13 +615,13 @@ export const executeClaudeCommand = async params => {
|
|
|
614
615
|
const progressMonitor = await initProgressMonitoring(argv, { owner, repo, prNumber, $, log }); // works with or without --interactive-mode
|
|
615
616
|
let execCommand;
|
|
616
617
|
const mappedModel = mapModelToId(argv.model);
|
|
618
|
+
const toolInvocation = resolveFormalAiToolInvocation({ tool: 'claude', model: argv.model, toolPath: claudePath });
|
|
617
619
|
const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
|
|
618
620
|
const resolvedSubAgentModel = argv.subAgentModel ? mapClaudeSubAgentModelToEnvValue(argv.subAgentModel) : undefined; // Issue #1978
|
|
619
621
|
const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
|
|
620
622
|
const resolvedExecutionModel = resolvedPlanModel ? mappedModel : undefined;
|
|
621
|
-
// Issue #1949: Let Claude Code handle transient
|
|
622
|
-
//
|
|
623
|
-
// `--model`. Only for plain `--model` runs (not opusplan) with a distinct fallback.
|
|
623
|
+
// Issue #1949: Let Claude Code's `--fallback-model` handle transient overloads and retry
|
|
624
|
+
// the primary each turn. Only for plain `--model` runs with a distinct fallback.
|
|
624
625
|
const mappedFallbackModel = argv.fallbackModel ? mapModelToId(argv.fallbackModel) : undefined;
|
|
625
626
|
const useClaudeFallbackModel = !resolvedPlanModel && mappedFallbackModel && mappedFallbackModel !== effectiveModel;
|
|
626
627
|
let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
|
|
@@ -650,10 +651,9 @@ export const executeClaudeCommand = async params => {
|
|
|
650
651
|
} else {
|
|
651
652
|
claudeArgs += ` -p "${escapedPromptForAttempt}" --append-system-prompt "${escapedSystemPrompt}"`;
|
|
652
653
|
}
|
|
653
|
-
const fullCommand = `(cd "${tempDir}" && ${
|
|
654
|
-
await
|
|
655
|
-
|
|
656
|
-
await log('');
|
|
654
|
+
const fullCommand = `(cd "${tempDir}" && ${toolInvocation.displayCommand} ${claudeArgs} | jq -c .)`;
|
|
655
|
+
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
656
|
+
if (preparedResult) return preparedResult;
|
|
657
657
|
if (argv.verbose) {
|
|
658
658
|
await log(`📋 User prompt:\n---BEGIN USER PROMPT---\n${promptForAttempt}\n---END USER PROMPT---`, { verbose: true });
|
|
659
659
|
await log(`📋 System prompt:\n---BEGIN SYSTEM PROMPT---\n${systemPrompt}\n---END SYSTEM PROMPT---`, { verbose: true });
|
|
@@ -684,15 +684,15 @@ export const executeClaudeCommand = async params => {
|
|
|
684
684
|
if (useClaudeFallbackModel && argv.verbose) await log(`📊 Claude --fallback-model: ${mappedFallbackModel} (Issue #1949 — primary --model ${effectiveModel} stays stable across overload retries)`, { verbose: true });
|
|
685
685
|
if (argv.resume) {
|
|
686
686
|
const simpleEscapedPrompt = promptForAttempt.replace(/"/g, '\\"');
|
|
687
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${
|
|
687
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, mirror: false, env: claudeEnv })`${toolInvocation.command} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
688
688
|
} else if (streamingInput) {
|
|
689
689
|
// Issue #817: Drive Claude via --input-format stream-json on a pipe
|
|
690
690
|
// stdin. Initial prompt + later PR comments are written as NDJSON
|
|
691
691
|
// frames by attachStreamingInput (see bidirectional-interactive.lib.mjs).
|
|
692
692
|
const streamingInputArgs = ['-p', '--input-format', 'stream-json'];
|
|
693
|
-
execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${
|
|
693
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
694
694
|
} else {
|
|
695
|
-
execCommand = $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${
|
|
695
|
+
execCommand = toolInvocation.formalAi ? $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${toolInvocation.command} ${toolInvocation.args} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"` : $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
696
696
|
}
|
|
697
697
|
if (streamingInput) {
|
|
698
698
|
await attachStreamingInput(bidirectionalHandler, execCommand, promptForAttempt, log, !!argv.verbose);
|
|
@@ -14,16 +14,20 @@ import path from 'node:path';
|
|
|
14
14
|
import { promisify } from 'node:util';
|
|
15
15
|
|
|
16
16
|
import { CODEX_PLUGIN_CLI, buildPluginCachePath as buildAgentPluginCachePath, buildPluginPayloadRepairs, pluginIdParts, readMaterializedPluginSkills as readAgentMaterializedPluginSkills, repairPluginPayloads } from './agent-plugin-cache.lib.mjs';
|
|
17
|
+
import { AGENTS_MD_FILENAMES, CLAUDE_MD_FILENAME } from './agents-md-claude-support.lib.mjs';
|
|
17
18
|
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
|
-
const REQUIREMENT_WORDS = /\b(?:depend(?:s|ency)?|install|invoke|mandatory|must|need(?:ed|s)?|preflight|required?|requires|
|
|
20
|
+
const REQUIREMENT_WORDS = /\b(?:depend(?:s|ency)?|install|invoke|mandatory|must|need(?:ed|s)?|preflight|required?|requires|us(?:e|es|ing))\b/i;
|
|
20
21
|
const NEGATED_REQUIREMENT = /\b(?:does\s+not\s+require|not\s+required|optional)\b/i;
|
|
21
22
|
// `(?!\.[a-z])` keeps email addresses and hostnames (`ops@example.com`) out of
|
|
22
23
|
// the plugin selector space.
|
|
23
24
|
const PLUGIN_SELECTOR = /\b([a-z0-9][a-z0-9-]*@[a-z0-9][a-z0-9-]*(?:-remote)?)\b(?!\.[a-z])/gi;
|
|
24
25
|
const NAMESPACED_SKILL = /\b([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*)\b/gi;
|
|
25
26
|
const EXPLICIT_BARE_SKILL = /\$([a-z0-9][a-z0-9-]*)|`([a-z0-9][a-z0-9-]*)`\s+(?:agent\s+)?skill/gi;
|
|
26
|
-
|
|
27
|
+
// Issue #2102: `us(?:e|ing)` covers the imperative form (`Use \`ns:skill\``) and
|
|
28
|
+
// the participle a repository AGENTS.md uses for the same instruction
|
|
29
|
+
// (`Execute approved plans in an isolated worktree using \`ns:skill\``).
|
|
30
|
+
const CAPABILITY_PREFIX = /\b(?:depend(?:s|ed)?\s+on|install|invoke|must\s+(?:install|invoke|use)|need(?:ed|s)?(?:\s+to)?(?:\s+(?:install|invoke|use))?|require(?:d|s)?(?:\s+to)?(?:\s+(?:install|invoke|use))?|us(?:e|ing))\s+(?:(?:the|an?)\s+)?(?:(?:agent\s+)?skill\s+)?(?:named\s+)?[`$]?$/i;
|
|
27
31
|
const CAPABILITY_SUFFIX = /^`?\s+(?:agent\s+)?(?:skill|capability)\b/i;
|
|
28
32
|
const STRUCTURED_DATA_VALUES = new Set(['array', 'boolean', 'false', 'integer', 'null', 'number', 'object', 'string', 'true']);
|
|
29
33
|
|
|
@@ -49,6 +53,11 @@ const PROSE_TOKENS = new Set(['agent', 'caution', 'codex', 'default', 'error', '
|
|
|
49
53
|
|
|
50
54
|
const isCapabilityToken = value => CAPABILITY_TOKEN.test(value) && !PROSE_TOKENS.has(value);
|
|
51
55
|
|
|
56
|
+
const skillParts = skill => {
|
|
57
|
+
const separator = skill.indexOf(':');
|
|
58
|
+
return separator === -1 ? { namespace: null, name: skill } : { namespace: skill.slice(0, separator), name: skill.slice(separator + 1) };
|
|
59
|
+
};
|
|
60
|
+
|
|
52
61
|
const hasExplicitCapabilityContext = (line, match) => {
|
|
53
62
|
const before = line.slice(0, match.index);
|
|
54
63
|
const after = line.slice(match.index + match[0].length);
|
|
@@ -89,7 +98,7 @@ export function normalizePluginSelector(selector) {
|
|
|
89
98
|
.replace(/@openai-curated-remote$/u, '@openai-curated');
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
export function detectRequiredCodexCapabilities(text) {
|
|
101
|
+
export function detectRequiredCodexCapabilities(text, { source = null } = {}) {
|
|
93
102
|
const plugins = new Set();
|
|
94
103
|
const skills = new Set();
|
|
95
104
|
// Issue #2088: a fully qualified reference — `plugin@marketplace` in plugin
|
|
@@ -106,12 +115,12 @@ export function detectRequiredCodexCapabilities(text) {
|
|
|
106
115
|
|
|
107
116
|
const accept = (target, value, line, { qualified = false } = {}) => {
|
|
108
117
|
if (!isCapabilityName(value)) {
|
|
109
|
-
rejected.push({ capability: value, line });
|
|
118
|
+
rejected.push({ capability: value, line, source });
|
|
110
119
|
return;
|
|
111
120
|
}
|
|
112
121
|
target.add(value);
|
|
113
122
|
if (qualified) explicit.add(value);
|
|
114
|
-
evidence.push({ capability: value, line, explicit: qualified });
|
|
123
|
+
evidence.push({ capability: value, line, explicit: qualified, source });
|
|
115
124
|
};
|
|
116
125
|
|
|
117
126
|
for (const rawLine of String(text || '').split(/\r?\n/u)) {
|
|
@@ -121,12 +130,22 @@ export function detectRequiredCodexCapabilities(text) {
|
|
|
121
130
|
for (const match of line.matchAll(PLUGIN_SELECTOR)) {
|
|
122
131
|
const selector = normalizePluginSelector(match[1]);
|
|
123
132
|
if (hasExplicitPluginContext(line, match)) accept(plugins, selector, line, { qualified: true });
|
|
124
|
-
else rejected.push({ capability: selector, line });
|
|
133
|
+
else rejected.push({ capability: selector, line, source });
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
// Issue #2102: a conjunction shares one requirement verb. `invoke `a:x` and
|
|
136
|
+
// `a:y`` only puts `a:x` in capability context, so a later reference to a
|
|
137
|
+
// namespace already qualified on the same line inherits the qualification
|
|
138
|
+
// instead of being dropped. Restricting inheritance to the same namespace,
|
|
139
|
+
// and still rejecting structured-data values, keeps issues #2077 and #2080
|
|
140
|
+
// from resurfacing: prose cannot produce a second `ns:name` token for a
|
|
141
|
+
// namespace that was just named in an explicit requirement.
|
|
142
|
+
const lineSkills = [...line.matchAll(NAMESPACED_SKILL)].map(match => ({ skill: match[1].toLowerCase(), qualified: hasExplicitCapabilityContext(line, match) }));
|
|
143
|
+
const qualifiedNamespaces = new Set(lineSkills.filter(entry => entry.qualified).map(entry => skillParts(entry.skill).namespace));
|
|
144
|
+
for (const entry of lineSkills) {
|
|
145
|
+
const { namespace, name } = skillParts(entry.skill);
|
|
146
|
+
const inherited = qualifiedNamespaces.has(namespace) && !STRUCTURED_DATA_VALUES.has(name);
|
|
147
|
+
if (entry.qualified || inherited) accept(skills, entry.skill, line, { qualified: true });
|
|
148
|
+
else rejected.push({ capability: entry.skill, line, source });
|
|
130
149
|
}
|
|
131
150
|
for (const match of line.matchAll(EXPLICIT_BARE_SKILL)) accept(skills, (match[1] || match[2]).toLowerCase(), line);
|
|
132
151
|
}
|
|
@@ -231,11 +250,6 @@ const verifyModelVisibleSkills = async ({ command, env, runCommand, log, require
|
|
|
231
250
|
throw skillVisibilityError({ missing: outcome.missing, visible: outcome.visible, requirements });
|
|
232
251
|
};
|
|
233
252
|
|
|
234
|
-
const skillParts = skill => {
|
|
235
|
-
const separator = skill.indexOf(':');
|
|
236
|
-
return separator === -1 ? { namespace: null, name: skill } : { namespace: skill.slice(0, separator), name: skill.slice(separator + 1) };
|
|
237
|
-
};
|
|
238
|
-
|
|
239
253
|
const pluginProvidesSkill = async (plugin, skill) => {
|
|
240
254
|
const sourcePath = plugin?.source?.path;
|
|
241
255
|
if (!sourcePath) return false;
|
|
@@ -316,14 +330,182 @@ export async function resolveRequiredPlugins(options) {
|
|
|
316
330
|
return (await resolveRequiredCapabilities(options)).plugins;
|
|
317
331
|
}
|
|
318
332
|
|
|
319
|
-
const
|
|
333
|
+
const readIssueRequirementSegments = async ({ owner, repo, issueNumber, runCommand, env }) => {
|
|
320
334
|
const issueResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}`], env });
|
|
321
335
|
const issue = parseJsonCommand(issueResult, 'Codex capability issue discovery');
|
|
322
336
|
const commentsResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}/comments`, '--paginate'], env });
|
|
323
337
|
const comments = parseJsonCommand(commentsResult, 'Codex capability comment discovery');
|
|
324
|
-
|
|
338
|
+
const bodies = (Array.isArray(comments) ? comments : []).map(comment => comment?.body).filter(Boolean);
|
|
339
|
+
return {
|
|
340
|
+
segments: [{ source: `issue #${issueNumber}`, text: [issue?.title, issue?.body].filter(Boolean).join('\n') }, ...bodies.map((text, index) => ({ source: `comment ${index + 1}`, text }))],
|
|
341
|
+
sources: [`issue #${issueNumber}`, ...(bodies.length > 0 ? [`${bodies.length} comment${bodies.length === 1 ? '' : 's'}`] : [])],
|
|
342
|
+
};
|
|
325
343
|
};
|
|
326
344
|
|
|
345
|
+
// Issue #2102: the requirement corpus has to include the instructions the target
|
|
346
|
+
// repository gives the agent. CEHR2005/GCS-TS#5 delegated its entire mandatory
|
|
347
|
+
// workflow to `AGENTS.md` ("Follow the repository and nested engine `AGENTS.md`
|
|
348
|
+
// instructions"), so an issue-only corpus detected nothing, provisioning was
|
|
349
|
+
// skipped, and the model was left to call `request_plugin_install` — which
|
|
350
|
+
// `codex exec` can never satisfy.
|
|
351
|
+
//
|
|
352
|
+
// `CLAUDE.md` is scanned for parity with `--tool claude` (see
|
|
353
|
+
// `agents-md-claude-support.lib.mjs`, which presents `AGENTS.md` under that name)
|
|
354
|
+
// and `.codex/*.md` for repositories that keep Codex-specific rules there.
|
|
355
|
+
const AGENT_INSTRUCTION_FILENAMES = new Set([...AGENTS_MD_FILENAMES, CLAUDE_MD_FILENAME, 'claude.md']);
|
|
356
|
+
const CODEX_INSTRUCTION_DIRECTORY = '.codex';
|
|
357
|
+
// Vendored dependencies, build output and VCS internals carry instructions that
|
|
358
|
+
// belong to other projects; reading them would invent requirements the task
|
|
359
|
+
// never had.
|
|
360
|
+
const SKIPPED_INSTRUCTION_DIRECTORIES = new Set(['__pycache__', 'build', 'coverage', 'dist', 'node_modules', 'out', 'target', 'tmp', 'vendor', 'venv']);
|
|
361
|
+
const WALKED_HIDDEN_DIRECTORIES = new Set([CODEX_INSTRUCTION_DIRECTORY, '.github']);
|
|
362
|
+
const INSTRUCTION_WALK_LIMITS = Object.freeze({ maxDepth: 3, maxFiles: 24, maxBytes: 256 * 1024 });
|
|
363
|
+
|
|
364
|
+
const isInstructionFile = ({ name, directoryName }) => AGENT_INSTRUCTION_FILENAMES.has(name) || (directoryName === CODEX_INSTRUCTION_DIRECTORY && name.toLowerCase().endsWith('.md'));
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Bounded walk over the checked-out repository's agent instruction files.
|
|
368
|
+
*
|
|
369
|
+
* Bounded on purpose: depth, file count and file size are all capped. A
|
|
370
|
+
* candidate dropped by the count or size cap is reported in `skipped`, so a
|
|
371
|
+
* truncated scan is visible in the log instead of looking like "nothing to
|
|
372
|
+
* find"; directories excluded by name or depth are not reported, because
|
|
373
|
+
* listing every skipped `node_modules` would bury that signal. Symlinked
|
|
374
|
+
* directories are not followed, which also makes the walk cycle-free.
|
|
375
|
+
*/
|
|
376
|
+
export async function collectAgentInstructionFiles({ projectDir, maxDepth = INSTRUCTION_WALK_LIMITS.maxDepth, maxFiles = INSTRUCTION_WALK_LIMITS.maxFiles, maxBytes = INSTRUCTION_WALK_LIMITS.maxBytes } = {}) {
|
|
377
|
+
const files = [];
|
|
378
|
+
const skipped = [];
|
|
379
|
+
if (!projectDir) return { files, skipped };
|
|
380
|
+
|
|
381
|
+
const candidates = [];
|
|
382
|
+
const walk = async (directory, relative, depth) => {
|
|
383
|
+
let entries;
|
|
384
|
+
try {
|
|
385
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const directoryName = relative ? path.posix.basename(relative) : '';
|
|
390
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
391
|
+
const relativePath = relative ? `${relative}/${entry.name}` : entry.name;
|
|
392
|
+
if (entry.isDirectory()) {
|
|
393
|
+
if (depth >= maxDepth) continue;
|
|
394
|
+
if (SKIPPED_INSTRUCTION_DIRECTORIES.has(entry.name)) continue;
|
|
395
|
+
if (entry.name.startsWith('.') && !WALKED_HIDDEN_DIRECTORIES.has(entry.name)) continue;
|
|
396
|
+
await walk(path.join(directory, entry.name), relativePath, depth + 1);
|
|
397
|
+
} else if (entry.isFile() && isInstructionFile({ name: entry.name, directoryName })) {
|
|
398
|
+
candidates.push({ relativePath, absolutePath: path.join(directory, entry.name), depth });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
await walk(projectDir, '', 0);
|
|
403
|
+
|
|
404
|
+
candidates.sort((left, right) => left.depth - right.depth || left.relativePath.localeCompare(right.relativePath));
|
|
405
|
+
for (const candidate of candidates) {
|
|
406
|
+
let stats;
|
|
407
|
+
try {
|
|
408
|
+
stats = await fs.stat(candidate.absolutePath);
|
|
409
|
+
} catch {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (stats.size > maxBytes) {
|
|
413
|
+
skipped.push({ relativePath: candidate.relativePath, reason: 'too-large', bytes: stats.size });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (files.length >= maxFiles) {
|
|
417
|
+
skipped.push({ relativePath: candidate.relativePath, reason: 'max-files' });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
files.push({ relativePath: candidate.relativePath, text: await fs.readFile(candidate.absolutePath, 'utf8'), bytes: stats.size });
|
|
422
|
+
} catch {
|
|
423
|
+
// An unreadable instruction file is not a requirement source.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { files, skipped };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Issue #2102 escape hatch: `--require-codex-plugin` /
|
|
431
|
+
* `HIVE_MIND_CODEX_REQUIRED_PLUGINS` let an operator state a requirement that no
|
|
432
|
+
* text declares. A selector without a marketplace cannot be installed by
|
|
433
|
+
* `codex plugin add`, so it is reported rather than silently ignored.
|
|
434
|
+
*/
|
|
435
|
+
export function parseRequiredCapabilityOverrides(value) {
|
|
436
|
+
const tokens = (Array.isArray(value) ? value : [value])
|
|
437
|
+
.flatMap(entry =>
|
|
438
|
+
String(entry ?? '')
|
|
439
|
+
.split(/[,;\s]+/u)
|
|
440
|
+
.map(token => token.trim())
|
|
441
|
+
)
|
|
442
|
+
.filter(Boolean);
|
|
443
|
+
const plugins = new Set();
|
|
444
|
+
const invalid = [];
|
|
445
|
+
for (const token of tokens) {
|
|
446
|
+
const selector = normalizePluginSelector(token);
|
|
447
|
+
if (selector.includes('@') && isCapabilityName(selector)) plugins.add(selector);
|
|
448
|
+
else invalid.push(token);
|
|
449
|
+
}
|
|
450
|
+
return { plugins: [...plugins].sort(), invalid };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Requirement corpus for the Codex capability preflight: the issue (title, body,
|
|
455
|
+
* comments), the checked-out repository's agent instruction files, and operator
|
|
456
|
+
* overrides. `sources` names everything that was scanned so a zero-requirement
|
|
457
|
+
* result is explainable from the log alone (issue #2102).
|
|
458
|
+
*/
|
|
459
|
+
export async function collectCodexCapabilityRequirements({ owner, repo, issueNumber, projectDir, runCommand = defaultRunCommand, env = process.env, requiredPlugins, instructionLimits } = {}) {
|
|
460
|
+
const segments = [];
|
|
461
|
+
const sources = [];
|
|
462
|
+
|
|
463
|
+
if (owner && repo && issueNumber) {
|
|
464
|
+
const issue = await readIssueRequirementSegments({ owner, repo, issueNumber, runCommand, env });
|
|
465
|
+
segments.push(...issue.segments);
|
|
466
|
+
sources.push(...issue.sources);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const instructions = await collectAgentInstructionFiles({ projectDir, ...instructionLimits });
|
|
470
|
+
for (const file of instructions.files) {
|
|
471
|
+
segments.push({ source: file.relativePath, text: file.text });
|
|
472
|
+
sources.push(file.relativePath);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const plugins = new Set();
|
|
476
|
+
const skills = new Set();
|
|
477
|
+
const explicit = new Set();
|
|
478
|
+
const evidence = [];
|
|
479
|
+
const rejected = [];
|
|
480
|
+
for (const segment of segments) {
|
|
481
|
+
const detected = detectRequiredCodexCapabilities(segment.text, { source: segment.source });
|
|
482
|
+
for (const plugin of detected.plugins) plugins.add(plugin);
|
|
483
|
+
for (const skill of detected.skills) skills.add(skill);
|
|
484
|
+
for (const capability of detected.explicit) explicit.add(capability);
|
|
485
|
+
evidence.push(...detected.evidence);
|
|
486
|
+
rejected.push(...detected.rejected);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
for (const { label, value } of [
|
|
490
|
+
{ label: '--require-codex-plugin', value: requiredPlugins },
|
|
491
|
+
{ label: 'HIVE_MIND_CODEX_REQUIRED_PLUGINS', value: env?.HIVE_MIND_CODEX_REQUIRED_PLUGINS },
|
|
492
|
+
]) {
|
|
493
|
+
const overrides = parseRequiredCapabilityOverrides(value);
|
|
494
|
+
if (overrides.plugins.length === 0 && overrides.invalid.length === 0) continue;
|
|
495
|
+
sources.push(label);
|
|
496
|
+
for (const selector of overrides.plugins) {
|
|
497
|
+
plugins.add(selector);
|
|
498
|
+
// An operator override is as explicit as a requirement gets, so it fails
|
|
499
|
+
// closed like any other declared capability (issue #2088).
|
|
500
|
+
explicit.add(selector);
|
|
501
|
+
evidence.push({ capability: selector, line: `${label}=${selector}`, explicit: true, source: label });
|
|
502
|
+
}
|
|
503
|
+
for (const token of overrides.invalid) rejected.push({ capability: token, line: `${label}=${token} (expected plugin@marketplace)`, source: label });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { plugins: [...plugins].sort(), skills: [...skills].sort(), explicit: [...explicit].sort(), evidence, rejected, sources, skippedInstructionFiles: instructions.skipped };
|
|
507
|
+
}
|
|
508
|
+
|
|
327
509
|
const replaceWithRelativeSymlink = async ({ source, target }) => {
|
|
328
510
|
try {
|
|
329
511
|
const current = await fs.readlink(target);
|
|
@@ -586,22 +768,35 @@ export async function runCodexCapabilityPreflight(options = {}) {
|
|
|
586
768
|
}
|
|
587
769
|
}
|
|
588
770
|
|
|
589
|
-
async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir, env = process.env, baseCodexHome = env.HIVE_MIND_PARENT_CODEX_HOME || env.CODEX_HOME || path.join(os.homedir(), '.codex'), codexPath = 'codex', runCommand = defaultRunCommand, log = async () => {} } = {}) {
|
|
590
|
-
|
|
771
|
+
async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir, requiredPlugins, env = process.env, baseCodexHome = env.HIVE_MIND_PARENT_CODEX_HOME || env.CODEX_HOME || path.join(os.homedir(), '.codex'), codexPath = 'codex', runCommand = defaultRunCommand, log = async () => {} } = {}) {
|
|
772
|
+
// Issue #2102: requirements no longer come from the issue alone, so a missing
|
|
773
|
+
// issue number (a pull-request continuation, for example) must not skip the
|
|
774
|
+
// repository's own AGENTS.md/CLAUDE.md files or an operator override. Owner and
|
|
775
|
+
// repo are still required: the scoped CODEX_HOME is keyed on them.
|
|
776
|
+
if (!owner || !repo) return { required: false, plugins: [], codexHome: null };
|
|
591
777
|
|
|
592
778
|
// `executeToolWithBun` uses a shell expression for execution. Preflight uses
|
|
593
779
|
// execFile and therefore selects the installed Codex binary directly.
|
|
594
780
|
const command = /\s/u.test(codexPath) ? 'codex' : codexPath;
|
|
595
|
-
const
|
|
596
|
-
const
|
|
597
|
-
for (const { capability, line } of requirements.rejected || []) {
|
|
598
|
-
await log(` ⏭️ Ignored non-capability token '${capability}' from: ${line.slice(0, 160)}`, { verbose: true });
|
|
781
|
+
const requirements = await collectCodexCapabilityRequirements({ owner, repo, issueNumber, projectDir, runCommand, env, requiredPlugins });
|
|
782
|
+
const scanned = `sources: ${requirements.sources.join(', ') || 'none'}`;
|
|
783
|
+
for (const { capability, line, source } of requirements.rejected || []) {
|
|
784
|
+
await log(` ⏭️ Ignored non-capability token '${capability}' from ${source || 'requirement text'}: ${line.slice(0, 160)}`, { verbose: true });
|
|
785
|
+
}
|
|
786
|
+
for (const { relativePath, reason, bytes } of requirements.skippedInstructionFiles || []) {
|
|
787
|
+
await log(` ⏭️ Skipped instruction file ${relativePath} (${reason}${reason === 'too-large' ? `: ${bytes} bytes` : ''})`, { verbose: true });
|
|
788
|
+
}
|
|
789
|
+
// Issue #2102: a silent early return made a missed requirement indistinguishable
|
|
790
|
+
// from a task that has none. Name what was scanned so the next report is
|
|
791
|
+
// diagnosable from the log alone.
|
|
792
|
+
if (requirements.plugins.length === 0 && requirements.skills.length === 0) {
|
|
793
|
+
await log(`🔌 Codex capability preflight: no plugin or skill requirements detected (${scanned})`, { verbose: true });
|
|
794
|
+
return { required: false, plugins: [], codexHome: null, sources: requirements.sources };
|
|
599
795
|
}
|
|
600
|
-
if (requirements.plugins.length === 0 && requirements.skills.length === 0) return { required: false, plugins: [], codexHome: null };
|
|
601
796
|
|
|
602
|
-
await log(`🔌 Codex capability preflight: detected ${requirements.plugins.length} plugin and ${requirements.skills.length} skill requirement(s)`);
|
|
603
|
-
for (const { capability, line } of requirements.evidence || []) {
|
|
604
|
-
await log(` 🔎 '${capability}' detected from: ${line.slice(0, 160)}`, { verbose: true });
|
|
797
|
+
await log(`🔌 Codex capability preflight: detected ${requirements.plugins.length} plugin and ${requirements.skills.length} skill requirement(s) (${scanned})`);
|
|
798
|
+
for (const { capability, line, source } of requirements.evidence || []) {
|
|
799
|
+
await log(` 🔎 '${capability}' detected from ${source || 'requirement text'}: ${line.slice(0, 160)}`, { verbose: true });
|
|
605
800
|
}
|
|
606
801
|
// Every Codex-side preflight operation runs from the checkout that the
|
|
607
802
|
// solver will enter. Besides matching `codex exec`, this keeps any future
|
|
@@ -677,9 +872,12 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
|
|
|
677
872
|
export default {
|
|
678
873
|
applyCodexCapabilityEnv,
|
|
679
874
|
buildPluginCachePath,
|
|
875
|
+
collectAgentInstructionFiles,
|
|
876
|
+
collectCodexCapabilityRequirements,
|
|
680
877
|
detectRequiredCodexCapabilities,
|
|
681
878
|
isCapabilityName,
|
|
682
879
|
isExplicitRequirement,
|
|
880
|
+
parseRequiredCapabilityOverrides,
|
|
683
881
|
readMaterializedPluginSkills,
|
|
684
882
|
repairScopedPluginPayloads,
|
|
685
883
|
resolveRequiredCapabilities,
|