@link-assistant/hive-mind 2.7.4 → 2.8.1

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,45 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 23a2187: Provision required Codex plugins and Agent Skills in persistent repository-scoped state before solver execution, including Docker propagation and actionable missing-capability diagnostics.
8
+
9
+ ## 2.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 8d02cf6: feat(fix): add `/fix --ci-cd <repository>` command (#1733)
14
+
15
+ `/fix --ci-cd` automatically generates and solves a CI/CD remediation issue for
16
+ a target repository:
17
+
18
+ - detects the repository's languages via the GitHub `/languages` API and selects
19
+ the matching CI/CD templates, ordered by detected language (byte-weighted);
20
+ - inspects the latest default-branch commit and lists its CI/CD runs in the issue
21
+ (falling back to the most recent default-branch runs when a release/tag commit
22
+ has none of its own);
23
+ - creates the remediation issue reusing the title and description of the standard
24
+ prompt (web-capture#139), with its retired case-study paragraph permanently
25
+ replaced by `--development-log` and the paragraphs supplied by `--deep-analysis`
26
+ omitted when that option is enabled. The issue is created as type `Bug`
27
+ (best-effort, since issue types are org-scoped) because that is what makes the
28
+ deep-analysis omission valid, and links to `docs/CI-CD-BEST-PRACTICES.md`;
29
+ - hands the issue off to `/solve --development-log --deep-analysis --auto-merge`,
30
+ forwarding every option `/fix` does not consume itself (e.g. `--tool`,
31
+ `--model`, `--think`);
32
+ - supports `--dry-run` (preview the issue) and `--no-solve` (create only).
33
+
34
+ The command is also available from the Telegram bot as `/fix <repository>`
35
+ (`--ci-cd` is implied), toggleable with `--no-fix` / `TELEGRAM_FIX=false` and
36
+ documented in all four locales.
37
+
38
+ Also adds the PHP template
39
+ (`link-foundation/php-ai-driven-development-pipeline-template`) to
40
+ `docs/CI-CD-BEST-PRACTICES.md` and documents the new "Automatic CI/CD
41
+ Remediation" flow in all four languages (en/zh/hi/ru).
42
+
3
43
  ## 2.7.4
4
44
 
5
45
  ### Patch Changes
package/README.hi.md CHANGED
@@ -210,14 +210,15 @@ docker attach hive-mind
210
210
  # --- Persisting auth data across restarts ---
211
211
 
212
212
  # Extract auth data from a running (or stopped) container to the host:
213
- mkdir -p ~/.hive-mind
213
+ mkdir -p ~/.hive-mind ~/.hive-mind/agents/skills
214
214
  docker cp hive-mind:/home/box/.claude ~/.hive-mind/claude
215
+ docker cp hive-mind:/home/box/.codex ~/.hive-mind/codex
215
216
  docker cp hive-mind:/home/box/.claude.json ~/.hive-mind/claude.json
216
217
  docker cp hive-mind:/home/box/.config/gh ~/.hive-mind/gh
217
218
 
218
219
  # Fix ownership to match the box user inside the container:
219
220
  BOX_UID=$(docker exec hive-mind id -u box)
220
- chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/gh
221
+ chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/codex ~/.hive-mind/agents ~/.hive-mind/gh
221
222
  chown $BOX_UID:$BOX_UID ~/.hive-mind/claude.json
222
223
 
223
224
  # On subsequent runs, mount the auth data to keep it between restarts:
@@ -225,11 +226,15 @@ docker run -dit \
225
226
  --name hive-mind \
226
227
  --restart unless-stopped \
227
228
  -v /root/.hive-mind/claude:/home/box/.claude \
229
+ -v /root/.hive-mind/codex:/home/box/.codex \
230
+ -v /root/.hive-mind/agents:/home/box/.agents \
228
231
  -v /root/.hive-mind/claude.json:/home/box/.claude.json \
229
232
  -v /root/.hive-mind/gh:/home/box/.config/gh \
230
233
  konard/hive-mind:latest
231
234
  ```
232
235
 
236
+ `codex exec` शुरू होने से पहले Hive Mind issue और उसकी comments में स्पष्ट plugin तथा Agent Skill requirements खोजता है। आवश्यक provider `/home/box/.codex/hive-mind/repositories/<owner>/<repo>` में install होता है, इसलिए configuration restart के बाद भी रहती है और दूसरे repositories को प्रभावित नहीं करती। `/home/box/.agents/skills` के user skills भी Docker-isolated tasks में उपलब्ध होते हैं; capability न मिलने पर preflight AI session से पहले exact identifier और remediation command बताता है।
237
+
233
238
  **Docker के लाभ:**
234
239
 
235
240
  - ✅ पूर्व-कॉन्फ़िगर Ubuntu 24.04 वातावरण
@@ -532,6 +537,28 @@ message पर `/merge` से reply भी कर सकते हैं जि
532
537
  mergeable होने तक wait करता है। Merge-conflict skips अभी भी `--auto-resolve` के
533
538
  साथ काम करते हैं।
534
539
 
540
+ #### `/fix` - CI/CD स्वतः ठीक करें
541
+
542
+ ```
543
+ /fix <github-repository-url> --ci-cd [options]
544
+
545
+ Examples:
546
+ /fix https://github.com/owner/repo --ci-cd
547
+ /fix owner/repo --ci-cd --tool codex --model gpt-5.5 --think max
548
+ /fix owner/repo --ci-cd --dry-run
549
+ /fix owner/repo --ci-cd --no-solve
550
+ ```
551
+
552
+ `/fix --ci-cd` लक्ष्य रिपॉज़िटरी की भाषाओं का पता लगाता है, डिफ़ॉल्ट ब्रांच के नवीनतम कमिट और उसके CI/CD रनों
553
+ की जाँच करता है, और स्वचालित रूप से एक CI/CD सुधार issue बनाता है (मानक प्रॉम्प्ट और
554
+ [`docs/CI-CD-BEST-PRACTICES.md`](docs/CI-CD-BEST-PRACTICES.md) के टेम्पलेट्स का उपयोग करके, जिन्हें पहचानी
555
+ गई भाषाओं के अनुसार क्रमबद्ध किया जाता है)। इसके बाद यह issue को
556
+ `/solve --development-log --deep-analysis --auto-merge` को सौंप देता है। हर वह विकल्प जिसे `/fix` स्वयं
557
+ उपयोग नहीं करता (जैसे `--tool`, `--model`, `--think`) `/solve` को अग्रेषित कर दिया जाता है। issue बनाए बिना
558
+ उसका पूर्वावलोकन करने के लिए `--dry-run` का उपयोग करें, या `/solve` शुरू किए बिना केवल issue बनाने के लिए
559
+ `--no-solve` का उपयोग करें। विवरण के लिए
560
+ [स्वचालित CI/CD सुधार](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation) देखें।
561
+
535
562
  #### `/limits` - उपयोग सीमाएँ दिखाएँ
536
563
 
537
564
  ```
package/README.md CHANGED
@@ -213,7 +213,7 @@ docker attach hive-mind
213
213
  # --- Persisting auth data across restarts ---
214
214
 
215
215
  # On the host, create the directories used by the current Docker workflow:
216
- mkdir -p /root/.hive-mind/claude /root/.hive-mind/codex /root/.hive-mind/gh
216
+ mkdir -p /root/.hive-mind/claude /root/.hive-mind/codex /root/.hive-mind/agents/skills /root/.hive-mind/gh
217
217
  touch -a /root/.hive-mind/claude.json
218
218
 
219
219
  # In our Docker images HOME=/home/box, so Codex stores its data in /home/box/.codex.
@@ -221,13 +221,14 @@ touch -a /root/.hive-mind/claude.json
221
221
  docker run -dit --user box --name hive-mind --restart unless-stopped \
222
222
  -v /root/.hive-mind/claude:/home/box/.claude \
223
223
  -v /root/.hive-mind/codex:/home/box/.codex \
224
+ -v /root/.hive-mind/agents:/home/box/.agents \
224
225
  -v /root/.hive-mind/claude.json:/home/box/.claude.json \
225
226
  -v /root/.hive-mind/gh:/home/box/.config/gh \
226
227
  konard/hive-mind:latest bash -l -c 'bash /home/box/start-bot.sh'
227
228
 
228
229
  # After the first start, fix ownership to match the box user inside the container:
229
230
  BOX_UID=$(docker exec hive-mind id -u box)
230
- chown -R $BOX_UID:$BOX_UID /root/.hive-mind/claude /root/.hive-mind/codex /root/.hive-mind/gh
231
+ chown -R $BOX_UID:$BOX_UID /root/.hive-mind/claude /root/.hive-mind/codex /root/.hive-mind/agents /root/.hive-mind/gh
231
232
  chown $BOX_UID:$BOX_UID /root/.hive-mind/claude.json
232
233
 
233
234
  # Important: mounted ~/.codex data overrides the image-baked Codex config.
@@ -246,6 +247,8 @@ docker exec -it hive-mind bash -lc 'codex mcp list && codex mcp add playwright -
246
247
 
247
248
  The Docker image itself now registers Playwright MCP for both Claude and Codex during build, and CI verifies those registrations in the built container. If `codex mcp list` is still empty in a running container, the usual cause is not the published image itself but a mounted `/home/box/.codex` directory from the host that replaces the image's default Codex configuration.
248
249
 
250
+ Before `codex exec`, Hive Mind scans the issue and its comments for explicit required plugin selectors and Agent Skill names. It resolves skill providers from Codex's marketplace catalog and installs them under `/home/box/.codex/hive-mind/repositories/<owner>/<repo>`, so enablement persists across restarts without affecting other repositories. Required user skills placed in `/home/box/.agents/skills` are also propagated to Docker-isolated tasks. An unavailable requirement stops before the AI session with the exact capability and a catalog remediation command.
251
+
249
252
  See [docs/DOCKER.md](./docs/DOCKER.md) for advanced Docker usage.
250
253
 
251
254
  #### Stoping and removing docker container
@@ -551,6 +554,30 @@ request link, such as a previous `/codex ...issues/123` command.
551
554
  If a target PR is not finished yet, `/merge` waits for it to become mergeable
552
555
  before merging. Merge-conflict skips still work with `--auto-resolve`.
553
556
 
557
+ #### `/fix` - Auto-Remediate CI/CD
558
+
559
+ ```
560
+ /fix <github-repository-url> --ci-cd [options]
561
+
562
+ Examples:
563
+ /fix https://github.com/owner/repo --ci-cd
564
+ /fix owner/repo --ci-cd --tool codex --model gpt-5.5 --think max
565
+ /fix owner/repo --ci-cd --dry-run
566
+ /fix owner/repo --ci-cd --no-solve
567
+ ```
568
+
569
+ `/fix --ci-cd` detects the target repository's languages, inspects the latest
570
+ default-branch commit and its CI/CD runs, and automatically creates a CI/CD
571
+ remediation issue (using the standard prompt and the templates from
572
+ [`docs/CI-CD-BEST-PRACTICES.md`](docs/CI-CD-BEST-PRACTICES.md), sorted by the
573
+ detected languages). It then hands the issue off to
574
+ `/solve --development-log --deep-analysis --auto-merge`. Every option `/fix`
575
+ does not consume itself (e.g. `--tool`, `--model`, `--think`) is forwarded to
576
+ `/solve`. Use `--dry-run` to preview the issue without creating it, or
577
+ `--no-solve` to create the issue without starting `/solve`. See
578
+ [Automatic CI/CD Remediation](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)
579
+ for details.
580
+
554
581
  #### `/limits` - Show Usage Limits
555
582
 
556
583
  ```
package/README.ru.md CHANGED
@@ -210,14 +210,15 @@ docker attach hive-mind
210
210
  # --- Persisting auth data across restarts ---
211
211
 
212
212
  # Extract auth data from a running (or stopped) container to the host:
213
- mkdir -p ~/.hive-mind
213
+ mkdir -p ~/.hive-mind ~/.hive-mind/agents/skills
214
214
  docker cp hive-mind:/home/box/.claude ~/.hive-mind/claude
215
+ docker cp hive-mind:/home/box/.codex ~/.hive-mind/codex
215
216
  docker cp hive-mind:/home/box/.claude.json ~/.hive-mind/claude.json
216
217
  docker cp hive-mind:/home/box/.config/gh ~/.hive-mind/gh
217
218
 
218
219
  # Fix ownership to match the box user inside the container:
219
220
  BOX_UID=$(docker exec hive-mind id -u box)
220
- chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/gh
221
+ chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/codex ~/.hive-mind/agents ~/.hive-mind/gh
221
222
  chown $BOX_UID:$BOX_UID ~/.hive-mind/claude.json
222
223
 
223
224
  # On subsequent runs, mount the auth data to keep it between restarts:
@@ -225,11 +226,15 @@ docker run -dit \
225
226
  --name hive-mind \
226
227
  --restart unless-stopped \
227
228
  -v /root/.hive-mind/claude:/home/box/.claude \
229
+ -v /root/.hive-mind/codex:/home/box/.codex \
230
+ -v /root/.hive-mind/agents:/home/box/.agents \
228
231
  -v /root/.hive-mind/claude.json:/home/box/.claude.json \
229
232
  -v /root/.hive-mind/gh:/home/box/.config/gh \
230
233
  konard/hive-mind:latest
231
234
  ```
232
235
 
236
+ Перед запуском `codex exec` Hive Mind ищет явные требования к плагинам и Agent Skills в issue и его комментариях. Нужный провайдер устанавливается в `/home/box/.codex/hive-mind/repositories/<owner>/<repo>`, поэтому конфигурация переживает перезапуски, но не влияет на другие репозитории. Пользовательские навыки из `/home/box/.agents/skills` также передаются в Docker-изолированные задачи; если capability недоступна, preflight до запуска ИИ-сессии сообщает точный идентификатор и команду исправления.
237
+
233
238
  **Преимущества Docker:**
234
239
 
235
240
  - ✅ Предварительно настроенная среда Ubuntu 24.04
@@ -533,6 +538,27 @@ pull request, например на предыдущую команду `/codex
533
538
  выполняет слияние. Пропуск конфликтов слияния по-прежнему работает с
534
539
  `--auto-resolve`.
535
540
 
541
+ #### `/fix` — Автоматическое исправление CI/CD
542
+
543
+ ```
544
+ /fix <github-repository-url> --ci-cd [options]
545
+
546
+ Examples:
547
+ /fix https://github.com/owner/repo --ci-cd
548
+ /fix owner/repo --ci-cd --tool codex --model gpt-5.5 --think max
549
+ /fix owner/repo --ci-cd --dry-run
550
+ /fix owner/repo --ci-cd --no-solve
551
+ ```
552
+
553
+ `/fix --ci-cd` определяет языки целевого репозитория, анализирует последний коммит ветки по умолчанию и его
554
+ запуски CI/CD и автоматически создаёт issue для исправления CI/CD (используя стандартный промпт и шаблоны из
555
+ [`docs/CI-CD-BEST-PRACTICES.md`](docs/CI-CD-BEST-PRACTICES.md), отсортированные по обнаруженным языкам). Затем
556
+ он передаёт issue в `/solve --development-log --deep-analysis --auto-merge`. Любая опция, которую `/fix` не
557
+ использует сам (например, `--tool`, `--model`, `--think`), передаётся в `/solve`. Используйте `--dry-run`,
558
+ чтобы предварительно просмотреть issue без его создания, или `--no-solve`, чтобы создать issue без запуска
559
+ `/solve`. Подробнее см.
560
+ [Автоматическое исправление CI/CD](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation).
561
+
536
562
  #### `/limits` — Показать лимиты использования
537
563
 
538
564
  ```
package/README.zh.md CHANGED
@@ -210,14 +210,15 @@ docker attach hive-mind
210
210
  # --- Persisting auth data across restarts ---
211
211
 
212
212
  # Extract auth data from a running (or stopped) container to the host:
213
- mkdir -p ~/.hive-mind
213
+ mkdir -p ~/.hive-mind ~/.hive-mind/agents/skills
214
214
  docker cp hive-mind:/home/box/.claude ~/.hive-mind/claude
215
+ docker cp hive-mind:/home/box/.codex ~/.hive-mind/codex
215
216
  docker cp hive-mind:/home/box/.claude.json ~/.hive-mind/claude.json
216
217
  docker cp hive-mind:/home/box/.config/gh ~/.hive-mind/gh
217
218
 
218
219
  # Fix ownership to match the box user inside the container:
219
220
  BOX_UID=$(docker exec hive-mind id -u box)
220
- chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/gh
221
+ chown -R $BOX_UID:$BOX_UID ~/.hive-mind/claude ~/.hive-mind/codex ~/.hive-mind/agents ~/.hive-mind/gh
221
222
  chown $BOX_UID:$BOX_UID ~/.hive-mind/claude.json
222
223
 
223
224
  # On subsequent runs, mount the auth data to keep it between restarts:
@@ -225,11 +226,15 @@ docker run -dit \
225
226
  --name hive-mind \
226
227
  --restart unless-stopped \
227
228
  -v /root/.hive-mind/claude:/home/box/.claude \
229
+ -v /root/.hive-mind/codex:/home/box/.codex \
230
+ -v /root/.hive-mind/agents:/home/box/.agents \
228
231
  -v /root/.hive-mind/claude.json:/home/box/.claude.json \
229
232
  -v /root/.hive-mind/gh:/home/box/.config/gh \
230
233
  konard/hive-mind:latest
231
234
  ```
232
235
 
236
+ 在启动 `codex exec` 之前,Hive Mind 会扫描 issue 及其评论中的显式插件和 Agent Skill 要求。它将所需提供程序安装到 `/home/box/.codex/hive-mind/repositories/<owner>/<repo>`,因此配置可跨重启持久化,但不会影响其他仓库。`/home/box/.agents/skills` 中的用户技能也会传入 Docker 隔离任务;若能力不可用,预检会在 AI 会话启动前报告确切标识符和修复命令。
237
+
233
238
  **Docker 的优势:**
234
239
 
235
240
  - ✅ 预配置的 Ubuntu 24.04 环境
@@ -528,6 +533,26 @@ Examples:
528
533
  如果目标 PR 尚未完成,`/merge` 会等待它变为可合并后再合并。合并冲突跳过仍可配合
529
534
  `--auto-resolve` 使用。
530
535
 
536
+ #### `/fix` - 自动修复 CI/CD
537
+
538
+ ```
539
+ /fix <github-repository-url> --ci-cd [options]
540
+
541
+ Examples:
542
+ /fix https://github.com/owner/repo --ci-cd
543
+ /fix owner/repo --ci-cd --tool codex --model gpt-5.5 --think max
544
+ /fix owner/repo --ci-cd --dry-run
545
+ /fix owner/repo --ci-cd --no-solve
546
+ ```
547
+
548
+ `/fix --ci-cd` 会检测目标仓库使用的语言,检查默认分支的最新提交及其 CI/CD 运行情况,并自动创建一个 CI/CD
549
+ 修复 issue(使用标准提示词,以及来自
550
+ [`docs/CI-CD-BEST-PRACTICES.md`](docs/CI-CD-BEST-PRACTICES.md) 的模板,按检测到的语言排序)。随后它会将该
551
+ issue 交给 `/solve --development-log --deep-analysis --auto-merge` 处理。所有 `/fix`
552
+ 自身不消费的选项(例如 `--tool`、`--model`、`--think`)都会转发给 `/solve`。使用 `--dry-run` 可在不创建
553
+ issue 的情况下预览,使用 `--no-solve` 可只创建 issue 而不启动 `/solve`。详见
554
+ [自动 CI/CD 修复](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)。
555
+
531
556
  #### `/limits` - 显示用量限制
532
557
 
533
558
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.7.4",
3
+ "version": "2.8.1",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -8,6 +8,7 @@
8
8
  "hive": "./src/hive.mjs",
9
9
  "solve": "./src/solve.mjs",
10
10
  "task": "./src/task.mjs",
11
+ "fix": "./src/fix.mjs",
11
12
  "hive-cleanup": "./src/cleanup.mjs",
12
13
  "review": "./src/review.mjs",
13
14
  "configure-claude": "./src/configure-claude.mjs",
@@ -28,7 +29,7 @@
28
29
  "changeset": "changeset",
29
30
  "changeset:version": "changeset version",
30
31
  "changeset:publish": "npm run build:pre && changeset publish",
31
- "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/configure-claude.mjs && chmod +x src/hive-screens.mjs",
32
+ "build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/fix.mjs && chmod +x src/configure-claude.mjs && chmod +x src/hive-screens.mjs",
32
33
  "prepare": "husky"
33
34
  },
34
35
  "repository": {
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Repository-scoped Codex capability preflight (issue #2074).
3
+ *
4
+ * Explicit plugin and Agent Skill requirements are discovered from the issue
5
+ * before `codex exec`. Required marketplace plugins are installed into a
6
+ * persistent CODEX_HOME scoped by repository, leaving the operator's global
7
+ * plugin enablement and the target repository untouched.
8
+ */
9
+
10
+ import { execFile } from 'node:child_process';
11
+ import fs from 'node:fs/promises';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import { promisify } from 'node:util';
15
+
16
+ const execFileAsync = promisify(execFile);
17
+ const REQUIREMENT_WORDS = /\b(?:depend(?:s|ency)?|install|invoke|mandatory|must|need(?:ed|s)?|preflight|required?|requires|use)\b/i;
18
+ const NEGATED_REQUIREMENT = /\b(?:does\s+not\s+require|not\s+required|optional)\b/i;
19
+ const PLUGIN_SELECTOR = /\b([a-z0-9][a-z0-9-]*@[a-z0-9][a-z0-9-]*(?:-remote)?)\b/gi;
20
+ const NAMESPACED_SKILL = /\b([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*)\b/gi;
21
+ const EXPLICIT_BARE_SKILL = /\$([a-z0-9][a-z0-9-]*)|`([a-z0-9][a-z0-9-]*)`\s+(?:agent\s+)?skill/gi;
22
+
23
+ export class CodexCapabilityPreflightError extends Error {
24
+ constructor(message, details = {}) {
25
+ super(message);
26
+ this.name = 'CodexCapabilityPreflightError';
27
+ this.details = details;
28
+ }
29
+ }
30
+
31
+ export function normalizePluginSelector(selector) {
32
+ return String(selector || '')
33
+ .trim()
34
+ .toLowerCase()
35
+ .replace(/@openai-curated-remote$/u, '@openai-curated');
36
+ }
37
+
38
+ export function detectRequiredCodexCapabilities(text) {
39
+ const plugins = new Set();
40
+ const skills = new Set();
41
+
42
+ for (const rawLine of String(text || '').split(/\r?\n/u)) {
43
+ const line = rawLine.trim();
44
+ if (!line || !REQUIREMENT_WORDS.test(line) || NEGATED_REQUIREMENT.test(line)) continue;
45
+
46
+ for (const match of line.matchAll(PLUGIN_SELECTOR)) plugins.add(normalizePluginSelector(match[1]));
47
+ for (const match of line.matchAll(NAMESPACED_SKILL)) skills.add(match[1].toLowerCase());
48
+
49
+ for (const match of line.matchAll(EXPLICIT_BARE_SKILL)) {
50
+ const name = (match[1] || match[2]).toLowerCase();
51
+ if (!['agent', 'codex', 'required', 'the'].includes(name)) skills.add(name);
52
+ }
53
+ }
54
+
55
+ return { plugins: [...plugins].sort(), skills: [...skills].sort() };
56
+ }
57
+
58
+ const sanitizePathSegment = value => String(value || '').replace(/[^a-zA-Z0-9._-]/gu, '_');
59
+
60
+ export function buildCodexCapabilityStatePath({ baseCodexHome, owner, repo }) {
61
+ return path.join(baseCodexHome, 'hive-mind', 'repositories', sanitizePathSegment(owner), sanitizePathSegment(repo));
62
+ }
63
+
64
+ export function applyCodexCapabilityEnv(env, { codexHome, baseCodexHome } = {}) {
65
+ if (!codexHome) return env;
66
+ return { ...env, CODEX_HOME: codexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
67
+ }
68
+
69
+ const defaultRunCommand = async ({ command, args, env = process.env, cwd }) => {
70
+ try {
71
+ const result = await execFileAsync(command, args, { cwd, env, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 });
72
+ return { stdout: result.stdout || '', stderr: result.stderr || '', code: 0 };
73
+ } catch (error) {
74
+ return { stdout: error.stdout || '', stderr: error.stderr || error.message, code: Number.isInteger(error.code) ? error.code : 1 };
75
+ }
76
+ };
77
+
78
+ const parseJsonCommand = (result, label) => {
79
+ if (result.code !== 0) throw new CodexCapabilityPreflightError(`${label} failed: ${String(result.stderr || result.stdout).trim()}`);
80
+ try {
81
+ return JSON.parse(result.stdout || 'null');
82
+ } catch (error) {
83
+ throw new CodexCapabilityPreflightError(`${label} returned invalid JSON: ${error.message}`);
84
+ }
85
+ };
86
+
87
+ const catalogEntries = catalog => [...(catalog?.installed || []), ...(catalog?.available || [])];
88
+
89
+ const skillParts = skill => {
90
+ const separator = skill.indexOf(':');
91
+ return separator === -1 ? { namespace: null, name: skill } : { namespace: skill.slice(0, separator), name: skill.slice(separator + 1) };
92
+ };
93
+
94
+ const pluginProvidesSkill = async (plugin, skill) => {
95
+ const sourcePath = plugin?.source?.path;
96
+ if (!sourcePath) return false;
97
+ const { name } = skillParts(skill);
98
+ try {
99
+ await fs.access(path.join(sourcePath, 'skills', name, 'SKILL.md'));
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ };
105
+
106
+ const skillExistsInDirectories = async (skill, skillDirectories) => {
107
+ const { name } = skillParts(skill);
108
+ for (const directory of skillDirectories || []) {
109
+ try {
110
+ await fs.access(path.join(directory, name, 'SKILL.md'));
111
+ return true;
112
+ } catch {
113
+ // Keep searching the remaining standard skill locations.
114
+ }
115
+ }
116
+ return false;
117
+ };
118
+
119
+ export async function resolveRequiredPlugins({ requirements, catalog, skillDirectories = [] }) {
120
+ const entries = catalogEntries(catalog);
121
+ const byId = new Map(entries.map(entry => [normalizePluginSelector(entry.pluginId), entry]));
122
+ const selected = new Map();
123
+ const missing = [];
124
+
125
+ for (const selector of requirements.plugins || []) {
126
+ const normalized = normalizePluginSelector(selector);
127
+ const entry = byId.get(normalized);
128
+ if (!entry) missing.push(normalized);
129
+ else selected.set(normalized, entry);
130
+ }
131
+
132
+ for (const skill of requirements.skills || []) {
133
+ const { namespace } = skillParts(skill);
134
+ if (!namespace && (await skillExistsInDirectories(skill, skillDirectories))) continue;
135
+ let candidates = entries;
136
+ if (namespace) candidates = entries.filter(entry => entry.name === namespace || String(entry.pluginId || '').startsWith(`${namespace}@`));
137
+
138
+ let provider = null;
139
+ for (const entry of candidates) {
140
+ if (await pluginProvidesSkill(entry, skill)) {
141
+ provider = entry;
142
+ break;
143
+ }
144
+ }
145
+ if (provider) selected.set(normalizePluginSelector(provider.pluginId), provider);
146
+ else missing.push(skill);
147
+ }
148
+
149
+ if (missing.length > 0) {
150
+ throw new CodexCapabilityPreflightError(`Required Codex capability unavailable: ${missing.join(', ')}. ` + `Run 'codex plugin list --available --json' in the operator container and configure a marketplace that provides it. ` + `Hive Mind installs discovered capabilities into repository-scoped CODEX_HOME state; it does not enable plugins globally.`, { missing });
151
+ }
152
+
153
+ return [...selected.keys()].sort();
154
+ }
155
+
156
+ const readIssueRequirementText = async ({ owner, repo, issueNumber, runCommand }) => {
157
+ const issueResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}`], env: process.env });
158
+ const issue = parseJsonCommand(issueResult, 'Codex capability issue discovery');
159
+ const commentsResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}/comments`, '--paginate'], env: process.env });
160
+ const comments = parseJsonCommand(commentsResult, 'Codex capability comment discovery');
161
+ return [issue?.title, issue?.body, ...(Array.isArray(comments) ? comments.map(comment => comment?.body) : [])].filter(Boolean).join('\n');
162
+ };
163
+
164
+ const replaceWithRelativeSymlink = async ({ source, target }) => {
165
+ try {
166
+ const current = await fs.readlink(target);
167
+ if (path.resolve(path.dirname(target), current) === path.resolve(source)) return;
168
+ } catch {
169
+ // Missing path or a non-symlink is reconciled below.
170
+ }
171
+ await fs.rm(target, { recursive: true, force: true });
172
+ await fs.mkdir(path.dirname(target), { recursive: true });
173
+ await fs.symlink(path.relative(path.dirname(target), source), target, 'dir');
174
+ };
175
+
176
+ const syncFileIfPresent = async (source, target) => {
177
+ try {
178
+ await fs.copyFile(source, target);
179
+ return true;
180
+ } catch (error) {
181
+ if (error.code === 'ENOENT') {
182
+ await fs.rm(target, { force: true });
183
+ return false;
184
+ }
185
+ throw error;
186
+ }
187
+ };
188
+
189
+ const readIfPresent = async filePath => {
190
+ try {
191
+ return await fs.readFile(filePath, 'utf8');
192
+ } catch (error) {
193
+ if (error.code === 'ENOENT') return '';
194
+ throw error;
195
+ }
196
+ };
197
+
198
+ // Runtime settings follow the operator config while plugin enablement remains
199
+ // persistent and isolated to this repository.
200
+ const syncScopedConfig = async ({ baseConfigPath, scopedConfigPath }) => {
201
+ const baseConfig = await readIfPresent(baseConfigPath);
202
+ const scopedConfig = await readIfPresent(scopedConfigPath);
203
+ const pluginPattern = /^\[plugins\."[^"]+"\][^\n]*(?:\n(?!\[)[^\n]*)*/gmu;
204
+ const pluginBlocks = scopedConfig.match(pluginPattern) || [];
205
+ const baseWithoutPlugins = baseConfig.replace(pluginPattern, '').trimEnd();
206
+ const nextConfig = [baseWithoutPlugins, ...pluginBlocks.map(block => block.trim())].filter(Boolean).join('\n\n');
207
+ if (nextConfig) await fs.writeFile(scopedConfigPath, `${nextConfig}\n`);
208
+ };
209
+
210
+ const prepareScopedCodexHome = async ({ baseCodexHome, codexHome }) => {
211
+ await fs.mkdir(codexHome, { recursive: true });
212
+ await syncScopedConfig({ baseConfigPath: path.join(baseCodexHome, 'config.toml'), scopedConfigPath: path.join(codexHome, 'config.toml') });
213
+ await syncFileIfPresent(path.join(baseCodexHome, 'auth.json'), path.join(codexHome, 'auth.json'));
214
+ await syncFileIfPresent(path.join(baseCodexHome, 'installation_id'), path.join(codexHome, 'installation_id'));
215
+
216
+ const marketplaceSource = path.join(baseCodexHome, '.tmp', 'plugins');
217
+ const marketplaceSha = path.join(baseCodexHome, '.tmp', 'plugins.sha');
218
+ try {
219
+ await fs.access(marketplaceSource);
220
+ await fs.access(marketplaceSha);
221
+ await replaceWithRelativeSymlink({ source: marketplaceSource, target: path.join(codexHome, '.tmp', 'plugins') });
222
+ await fs.copyFile(marketplaceSha, path.join(codexHome, '.tmp', 'plugins.sha'));
223
+ } catch {
224
+ throw new CodexCapabilityPreflightError(`The operator Codex marketplace snapshot is unavailable at ${marketplaceSource}. ` + `Run 'codex plugin list --available --json' once in the operator container, then retry.`);
225
+ }
226
+ };
227
+
228
+ export async function runCodexCapabilityPreflight({ owner, repo, issueNumber, projectDir, baseCodexHome = process.env.HIVE_MIND_PARENT_CODEX_HOME || process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), codexPath = 'codex', runCommand = defaultRunCommand, log = async () => {} } = {}) {
229
+ if (!owner || !repo || !issueNumber) return { required: false, plugins: [], codexHome: null };
230
+
231
+ // `executeToolWithBun` uses a shell expression for execution. Preflight uses
232
+ // execFile and therefore selects the installed Codex binary directly.
233
+ const command = /\s/u.test(codexPath) ? 'codex' : codexPath;
234
+ const requirementText = await readIssueRequirementText({ owner, repo, issueNumber, runCommand });
235
+ const requirements = detectRequiredCodexCapabilities(requirementText);
236
+ if (requirements.plugins.length === 0 && requirements.skills.length === 0) return { required: false, plugins: [], codexHome: null };
237
+
238
+ await log(`🔌 Codex capability preflight: detected ${requirements.plugins.length} plugin and ${requirements.skills.length} skill requirement(s)`);
239
+ const baseEnv = { ...process.env, CODEX_HOME: baseCodexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
240
+ const baseCatalogResult = await runCommand({ command, args: ['plugin', 'list', '--available', '--json'], env: baseEnv });
241
+ const baseCatalog = parseJsonCommand(baseCatalogResult, 'Codex plugin catalog discovery');
242
+ const skillDirectories = [path.join(os.homedir(), '.agents', 'skills'), projectDir && path.join(projectDir, '.agents', 'skills')].filter(Boolean);
243
+ const plugins = await resolveRequiredPlugins({ requirements, catalog: baseCatalog, skillDirectories });
244
+ if (plugins.length === 0) {
245
+ await log(' ✅ Required Agent Skills are already available from standard skill directories');
246
+ return { required: true, plugins, skills: requirements.skills, codexHome: null, baseCodexHome };
247
+ }
248
+
249
+ const codexHome = buildCodexCapabilityStatePath({ baseCodexHome, owner, repo });
250
+ await prepareScopedCodexHome({ baseCodexHome, codexHome });
251
+ const scopedEnv = { ...process.env, CODEX_HOME: codexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
252
+
253
+ const scopedCatalogResult = await runCommand({ command, args: ['plugin', 'list', '--json'], env: scopedEnv });
254
+ const scopedCatalog = parseJsonCommand(scopedCatalogResult, 'Repository-scoped Codex plugin discovery');
255
+ const installed = new Set((scopedCatalog.installed || []).filter(plugin => plugin.installed && plugin.enabled).map(plugin => normalizePluginSelector(plugin.pluginId)));
256
+
257
+ for (const plugin of plugins) {
258
+ if (installed.has(plugin)) continue;
259
+ const installResult = await runCommand({ command, args: ['plugin', 'add', plugin, '--json'], env: scopedEnv });
260
+ parseJsonCommand(installResult, `Installing required Codex plugin ${plugin}`);
261
+ await log(` ✅ Provisioned ${plugin} in repository-scoped Codex state`);
262
+ }
263
+
264
+ const verifyResult = await runCommand({ command, args: ['plugin', 'list', '--json'], env: scopedEnv });
265
+ const verifiedCatalog = parseJsonCommand(verifyResult, 'Codex capability verification');
266
+ const verified = new Set((verifiedCatalog.installed || []).filter(plugin => plugin.installed && plugin.enabled).map(plugin => normalizePluginSelector(plugin.pluginId)));
267
+ const unverified = plugins.filter(plugin => !verified.has(plugin));
268
+ if (unverified.length > 0) throw new CodexCapabilityPreflightError(`Codex capability installation did not verify successfully: ${unverified.join(', ')}`, { missing: unverified });
269
+
270
+ await log(` Codex capability state: ${codexHome}`, { verbose: true });
271
+ return { required: true, plugins, skills: requirements.skills, codexHome, baseCodexHome };
272
+ }
273
+
274
+ export default { applyCodexCapabilityEnv, detectRequiredCodexCapabilities, runCodexCapabilityPreflight };
package/src/codex.lib.mjs CHANGED
@@ -35,6 +35,7 @@ import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } fro
35
35
  import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
36
36
  import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
37
37
  import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
38
+ import { applyCodexCapabilityEnv, runCodexCapabilityPreflight } from './codex-capability-preflight.lib.mjs'; // Issue #2074
38
39
  import { createPullRequestBaseBranchCommandIntervention } from './solve.pr-base-command-intervention.lib.mjs';
39
40
  import Decimal from 'decimal.js-light';
40
41
 
@@ -744,6 +745,7 @@ export const executeCodex = async params => {
744
745
  // it natively from .agents/skills/handoff/SKILL.md (no-op unless --use-handoff).
745
746
  await deployHandoffSkill({ tempDir, argv, log, $ });
746
747
 
748
+ const capabilityPreflight = await runCodexCapabilityPreflight({ owner, repo, issueNumber, projectDir: tempDir, codexPath, log });
747
749
  // Execute the Codex command
748
750
  return await executeCodexCommand({
749
751
  tempDir,
@@ -761,11 +763,12 @@ export const executeCodex = async params => {
761
763
  owner,
762
764
  repo,
763
765
  prNumber,
766
+ capabilityPreflight,
764
767
  });
765
768
  };
766
769
 
767
770
  export const executeCodexCommand = async params => {
768
- const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, codexPath, $, owner, repo, prNumber, calculatePricing = calculateCodexPricing, waitForRetryDelay = waitWithCountdown } = params;
771
+ const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, codexPath, $, owner, repo, prNumber, capabilityPreflight, calculatePricing = calculateCodexPricing, waitForRetryDelay = waitWithCountdown } = params;
769
772
 
770
773
  const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
771
774
  const expectedBaseBranch = String(argv?.baseBranch || '').trim();
@@ -806,7 +809,10 @@ export const executeCodexCommand = async params => {
806
809
  const mappedModel = mapModelToId(argv.model);
807
810
  const { reasoningEffort, source: reasoningEffortSource, rolloutTokenBudget } = resolveCodexReasoningEffort(argv);
808
811
  const isResumeMode = !!argv.resume;
809
- const codexEnv = getCodexExecEnv(argv.verbose);
812
+ const codexEnv = applyCodexCapabilityEnv(getCodexExecEnv(argv.verbose), {
813
+ codexHome: capabilityPreflight?.codexHome,
814
+ baseCodexHome: capabilityPreflight?.baseCodexHome,
815
+ });
810
816
 
811
817
  // For Codex, we combine system and user prompts into a single message
812
818
  // Codex doesn't have separate system prompt support in CLI mode