@link-assistant/hive-mind 2.8.10 → 2.8.11

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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 2.8.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 55a318d: Keep locally provisioned `openai-curated` plugins visible when Codex's authenticated remote plugin catalog is enabled. The repository-scoped `remote_plugin` override is written in place for every TOML spelling of the setting, so an operator config that uses a dotted key or an inline table cannot produce a duplicate key that Codex refuses to load.
8
+
3
9
  ## 2.8.10
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.8.10",
3
+ "version": "2.8.11",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -219,7 +219,7 @@ const checkModelVisibleSkills = async ({ command, env, runCommand, log, required
219
219
  return { status: missing.length === 0 ? 'satisfied' : 'missing', visible, missing };
220
220
  };
221
221
 
222
- const skillVisibilityError = ({ missing, visible, requirements, repairs = [] }) => new CodexCapabilityPreflightError(`Codex reports the required plugins as installed, but the model cannot see: ${missing.join(', ')}. ` + `Codex exposes a plugin's skills only while its payload is materialized under ` + `CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>/skills. ` + `Visible skills were: ${visible ? [...visible].sort().join(', ') || 'none' : 'unknown'}.` + (repairs.length > 0 ? ` Attempted repairs: ${repairs.join(', ')}.` : ''), { missing, failClosed: missing.some(skill => isExplicitRequirement(requirements, skill)) });
222
+ const skillVisibilityError = ({ missing, visible, requirements, repairs = [] }) => new CodexCapabilityPreflightError(`Codex reports the required plugins as installed, but the model cannot see: ${missing.join(', ')}. ` + `A plugin must survive Codex loader reconciliation and have a valid payload under ` + `CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>/skills before its skills are exposed. ` + `Visible skills were: ${visible ? [...visible].sort().join(', ') || 'none' : 'unknown'}.` + (repairs.length > 0 ? ` Attempted repairs: ${repairs.join(', ')}.` : ''), { missing, failClosed: missing.some(skill => isExplicitRequirement(requirements, skill)) });
223
223
 
224
224
  const verifyModelVisibleSkills = async ({ command, env, runCommand, log, requiredSkills, requirements }) => {
225
225
  const outcome = await checkModelVisibleSkills({ command, env, runCommand, log, requiredSkills });
@@ -316,10 +316,10 @@ export async function resolveRequiredPlugins(options) {
316
316
  return (await resolveRequiredCapabilities(options)).plugins;
317
317
  }
318
318
 
319
- const readIssueRequirementText = async ({ owner, repo, issueNumber, runCommand }) => {
320
- const issueResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}`], env: process.env });
319
+ const readIssueRequirementText = async ({ owner, repo, issueNumber, runCommand, env }) => {
320
+ const issueResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}`], env });
321
321
  const issue = parseJsonCommand(issueResult, 'Codex capability issue discovery');
322
- const commentsResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}/comments`, '--paginate'], env: process.env });
322
+ const commentsResult = await runCommand({ command: 'gh', args: ['api', `repos/${owner}/${repo}/issues/${issueNumber}/comments`, '--paginate'], env });
323
323
  const comments = parseJsonCommand(commentsResult, 'Codex capability comment discovery');
324
324
  return [issue?.title, issue?.body, ...(Array.isArray(comments) ? comments.map(comment => comment?.body) : [])].filter(Boolean).join('\n');
325
325
  };
@@ -358,6 +358,128 @@ const readIfPresent = async filePath => {
358
358
  }
359
359
  };
360
360
 
361
+ const escapeRegExp = value => String(value).replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
362
+
363
+ // A line inside a multi-line string is data, not structure: `[plugins."x"]`
364
+ // written inside `"""…"""` is a string, and treating it as a table header would
365
+ // splice a key into the operator's value.
366
+ const markStructuralLines = lines => {
367
+ const structural = [];
368
+ let openDelimiter = null;
369
+ for (const line of lines) {
370
+ structural.push(openDelimiter === null);
371
+ let rest = line;
372
+ for (;;) {
373
+ if (openDelimiter) {
374
+ const closeIndex = rest.indexOf(openDelimiter);
375
+ if (closeIndex === -1) break;
376
+ rest = rest.slice(closeIndex + openDelimiter.length);
377
+ openDelimiter = null;
378
+ continue;
379
+ }
380
+ const opened = /"""|'''/u.exec(rest);
381
+ if (!opened) break;
382
+ openDelimiter = opened[0];
383
+ rest = rest.slice(opened.index + opened[0].length);
384
+ }
385
+ }
386
+ return structural;
387
+ };
388
+
389
+ // TOML accepts the same setting as a header table, a dotted key, or an inline
390
+ // table. Appending `[features]` next to any of the other two is a duplicate-key
391
+ // document that Codex refuses to load, which would break every scoped
392
+ // invocation for the repository — so each spelling is edited in place.
393
+ export const setTomlTableBoolean = ({ config, table, key, value }) => {
394
+ const lines = String(config || '')
395
+ .replace(/\r\n/gu, '\n')
396
+ .split('\n');
397
+ const structural = markStructuralLines(lines);
398
+ const [escapedTable, escapedKey] = [escapeRegExp(table), escapeRegExp(key)];
399
+ const quoted = name => `(?:${name}|"${name}"|'${name}')`;
400
+ const tableHeaderPattern = new RegExp(`^\\s*\\[\\s*${quoted(escapedTable)}\\s*\\]\\s*(?:#.*)?$`, 'u');
401
+ const anyTableHeaderPattern = /^\s*\[\[?[^\]]*\]\]?\s*(?:#.*)?$/u;
402
+ // The value is everything before an optional trailing comment, so a comment
403
+ // an operator attached to the setting survives the rewrite.
404
+ const assignmentPattern = name => new RegExp(`^(\\s*${name}\\s*=\\s*)([^#]*?)(\\s*(?:#.*)?)$`, 'u');
405
+ const keyPattern = assignmentPattern(quoted(escapedKey));
406
+ const dottedPattern = assignmentPattern(`${quoted(escapedTable)}\\s*\\.\\s*${quoted(escapedKey)}`);
407
+ const inlineTablePattern = new RegExp(`^(\\s*${quoted(escapedTable)}\\s*=\\s*\\{)([^}]*)(\\}\\s*(?:#.*)?)$`, 'u');
408
+ const finish = () => `${lines.join('\n').trimEnd()}\n`;
409
+ const isStructural = index => structural[index];
410
+
411
+ const tableStart = lines.findIndex((line, index) => isStructural(index) && tableHeaderPattern.test(line));
412
+ if (tableStart !== -1) {
413
+ let tableEnd = lines.length;
414
+ for (let index = tableStart + 1; index < lines.length; index++) {
415
+ if (isStructural(index) && anyTableHeaderPattern.test(lines[index])) {
416
+ tableEnd = index;
417
+ break;
418
+ }
419
+ }
420
+ for (let index = tableStart + 1; index < tableEnd; index++) {
421
+ // Any existing value is replaced, not only a boolean literal: leaving a
422
+ // stale `remote_plugin = 1` behind and appending a second assignment is
423
+ // also a duplicate key.
424
+ if (isStructural(index) && keyPattern.test(lines[index])) {
425
+ lines[index] = lines[index].replace(keyPattern, `$1${value}$3`);
426
+ return finish();
427
+ }
428
+ }
429
+ // Insert after the table's last content line so the key is not separated
430
+ // from its own table by the blank lines that precede the next one.
431
+ let insertAt = tableEnd;
432
+ while (insertAt > tableStart + 1 && lines[insertAt - 1].trim() === '') insertAt--;
433
+ lines.splice(insertAt, 0, `${key} = ${value}`);
434
+ return finish();
435
+ }
436
+
437
+ // Outside any table header the operator may have used a dotted key or an
438
+ // inline table for the same setting.
439
+ const rootEnd = lines.findIndex((line, index) => isStructural(index) && anyTableHeaderPattern.test(line));
440
+ const rootLimit = rootEnd === -1 ? lines.length : rootEnd;
441
+ for (let index = 0; index < rootLimit; index++) {
442
+ if (!isStructural(index)) continue;
443
+ if (dottedPattern.test(lines[index])) {
444
+ lines[index] = lines[index].replace(dottedPattern, `$1${value}$3`);
445
+ return finish();
446
+ }
447
+ const inlineTable = inlineTablePattern.exec(lines[index]);
448
+ if (inlineTable) {
449
+ const [, prefix, contents, suffix] = inlineTable;
450
+ // The trailing separator is captured so the operator's spacing inside the
451
+ // braces survives the rewrite.
452
+ const inlineKeyPattern = new RegExp(`(^|,)(\\s*${quoted(escapedKey)}\\s*=\\s*)[^,]*?(\\s*)(,|$)`, 'u');
453
+ const nextContents = inlineKeyPattern.test(contents) ? contents.replace(inlineKeyPattern, `$1$2${value}$3$4`) : `${contents.trim() ? `${contents.trimEnd()}, ` : ' '}${key} = ${value} `;
454
+ lines[index] = `${prefix}${nextContents}${suffix}`;
455
+ return finish();
456
+ }
457
+ }
458
+
459
+ while (lines.length > 0 && lines.at(-1) === '') lines.pop();
460
+ if (lines.length > 0) lines.push('');
461
+ lines.push(`[${table}]`, `${key} = ${value}`, '');
462
+ return lines.join('\n');
463
+ };
464
+
465
+ // Codex's authenticated remote global catalog owns the reserved
466
+ // `openai-curated` marketplace. In codex-rs 0.144.6 the prompt loader removes
467
+ // every locally configured plugin from that marketplace before merging remote
468
+ // installations. Hive provisions the local marketplace payload, so the scoped
469
+ // home must select that local catalog or plugin list and prompt assembly report
470
+ // contradictory states (issue #2094). This override is deliberately scoped to
471
+ // runs that selected a local curated plugin; personal marketplaces retain the
472
+ // operator's remote catalog setting.
473
+ const configureScopedPluginLoader = async ({ codexHome, plugins, log }) => {
474
+ const localCurated = plugins.filter(plugin => pluginIdParts(plugin).marketplace === 'openai-curated');
475
+ if (localCurated.length === 0) return;
476
+ const configPath = path.join(codexHome, 'config.toml');
477
+ const config = await readIfPresent(configPath);
478
+ const nextConfig = setTomlTableBoolean({ config, table: 'features', key: 'remote_plugin', value: false });
479
+ if (nextConfig !== config) await fs.writeFile(configPath, nextConfig);
480
+ await log(` 🧭 Scoped Codex loader: remote_plugin=false for ${localCurated.join(', ')}; an authenticated remote catalog otherwise removes local @openai-curated entries before prompt assembly`, { verbose: true });
481
+ };
482
+
361
483
  // Runtime settings follow the operator config while plugin enablement remains
362
484
  // persistent and isolated to this repository.
363
485
  const syncScopedConfig = async ({ baseConfigPath, scopedConfigPath }) => {
@@ -464,13 +586,13 @@ export async function runCodexCapabilityPreflight(options = {}) {
464
586
  }
465
587
  }
466
588
 
467
- async function provisionCodexCapabilities({ 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 () => {} } = {}) {
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 () => {} } = {}) {
468
590
  if (!owner || !repo || !issueNumber) return { required: false, plugins: [], codexHome: null };
469
591
 
470
592
  // `executeToolWithBun` uses a shell expression for execution. Preflight uses
471
593
  // execFile and therefore selects the installed Codex binary directly.
472
594
  const command = /\s/u.test(codexPath) ? 'codex' : codexPath;
473
- const requirementText = await readIssueRequirementText({ owner, repo, issueNumber, runCommand });
595
+ const requirementText = await readIssueRequirementText({ owner, repo, issueNumber, runCommand, env });
474
596
  const requirements = detectRequiredCodexCapabilities(requirementText);
475
597
  for (const { capability, line } of requirements.rejected || []) {
476
598
  await log(` ⏭️ Ignored non-capability token '${capability}' from: ${line.slice(0, 160)}`, { verbose: true });
@@ -481,8 +603,13 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
481
603
  for (const { capability, line } of requirements.evidence || []) {
482
604
  await log(` 🔎 '${capability}' detected from: ${line.slice(0, 160)}`, { verbose: true });
483
605
  }
484
- const baseEnv = { ...process.env, CODEX_HOME: baseCodexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
485
- const baseCatalogResult = await runCommand({ command, args: ['plugin', 'list', '--available', '--json'], env: baseEnv });
606
+ // Every Codex-side preflight operation runs from the checkout that the
607
+ // solver will enter. Besides matching `codex exec`, this keeps any future
608
+ // repository-local discovery deterministic without relying on Hive's launch
609
+ // directory (issue #2094).
610
+ const runCodexCommand = invocation => runCommand({ ...invocation, cwd: projectDir });
611
+ const baseEnv = { ...env, CODEX_HOME: baseCodexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
612
+ const baseCatalogResult = await runCodexCommand({ command, args: ['plugin', 'list', '--available', '--json'], env: baseEnv });
486
613
  const baseCatalog = parseJsonCommand(baseCatalogResult, 'Codex plugin catalog discovery');
487
614
  const skillDirectories = [path.join(os.homedir(), '.agents', 'skills'), projectDir && path.join(projectDir, '.agents', 'skills')].filter(Boolean);
488
615
  const { plugins, providers } = await resolveRequiredCapabilities({ requirements, catalog: baseCatalog, skillDirectories });
@@ -491,24 +618,25 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
491
618
  }
492
619
  if (plugins.length === 0) {
493
620
  await log(' ✅ Required Agent Skills are already available from standard skill directories');
494
- await verifyModelVisibleSkills({ command, env: baseEnv, runCommand, log, requiredSkills: requirements.skills, requirements });
621
+ await verifyModelVisibleSkills({ command, env: baseEnv, runCommand: runCodexCommand, log, requiredSkills: requirements.skills, requirements });
495
622
  return { required: true, plugins, skills: requirements.skills, codexHome: null, baseCodexHome };
496
623
  }
497
624
 
498
625
  const codexHome = buildCodexCapabilityStatePath({ baseCodexHome, owner, repo });
499
626
  await prepareScopedCodexHome({ baseCodexHome, codexHome });
500
- const scopedEnv = { ...process.env, CODEX_HOME: codexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
627
+ await configureScopedPluginLoader({ codexHome, plugins, log });
628
+ const scopedEnv = { ...env, CODEX_HOME: codexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
501
629
 
502
630
  // Issue #2088: install *and repair*. Enablement recorded in the scoped
503
631
  // `config.toml` survives a container restart while the payload under
504
632
  // `plugins/cache` may not, and `codex plugin list` cannot tell those states
505
633
  // apart — so the payload itself is the thing that gets checked and rebuilt.
506
- const repair = await repairScopedPluginPayloads({ command, env: scopedEnv, runCommand, log, codexHome, baseCodexHome, plugins, providers });
634
+ const repair = await repairScopedPluginPayloads({ command, env: scopedEnv, runCommand: runCodexCommand, log, codexHome, baseCodexHome, plugins, providers });
507
635
  for (const entry of repair.report) {
508
636
  if (entry.healthy) await log(` ✅ Provisioned ${entry.pluginId} in repository-scoped Codex state`);
509
637
  }
510
638
 
511
- const verifyResult = await runCommand({ command, args: ['plugin', 'list', '--json'], env: scopedEnv });
639
+ const verifyResult = await runCodexCommand({ command, args: ['plugin', 'list', '--json'], env: scopedEnv });
512
640
  const verifiedCatalog = parseJsonCommand(verifyResult, 'Codex capability verification');
513
641
  const verified = new Set((verifiedCatalog.installed || []).filter(plugin => plugin.installed && plugin.enabled).map(plugin => normalizePluginSelector(plugin.pluginId)));
514
642
  const unverified = plugins.filter(plugin => !verified.has(plugin));
@@ -525,14 +653,14 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
525
653
  // the model saw zero `superpowers:*` skills, so the run proceeded and then
526
654
  // stalled on the repository's mandatory preflight. Confirm the requirement
527
655
  // against the catalog the model actually receives.
528
- let visibility = await checkModelVisibleSkills({ command, env: scopedEnv, runCommand, log, requiredSkills: requirements.skills });
656
+ let visibility = await checkModelVisibleSkills({ command, env: scopedEnv, runCommand: runCodexCommand, log, requiredSkills: requirements.skills });
529
657
  if (visibility.status === 'missing') {
530
658
  // The payload looks materialized but the prompt disagrees: rebuild it from
531
659
  // scratch and re-probe before deciding (issue #2088).
532
660
  await log(` 🛠️ Model cannot see ${visibility.missing.join(', ')}; forcing a repository-scoped plugin payload rebuild`);
533
- const forced = await repairScopedPluginPayloads({ command, env: scopedEnv, runCommand, log, codexHome, baseCodexHome, plugins, providers, strategies: PLUGIN_PAYLOAD_REPAIRS.slice(1), force: true });
661
+ const forced = await repairScopedPluginPayloads({ command, env: scopedEnv, runCommand: runCodexCommand, log, codexHome, baseCodexHome, plugins, providers, strategies: PLUGIN_PAYLOAD_REPAIRS.slice(1), force: true });
534
662
  repair.applied.push(...forced.applied);
535
- visibility = await checkModelVisibleSkills({ command, env: scopedEnv, runCommand, log, requiredSkills: requirements.skills });
663
+ visibility = await checkModelVisibleSkills({ command, env: scopedEnv, runCommand: runCodexCommand, log, requiredSkills: requirements.skills });
536
664
  }
537
665
  if (visibility.status === 'missing') throw skillVisibilityError({ missing: visibility.missing, visible: visibility.visible, requirements, repairs: repair.applied });
538
666
  if (visibility.status === 'unknown' && repair.unhealthy.length > 0) {
@@ -556,4 +684,5 @@ export default {
556
684
  repairScopedPluginPayloads,
557
685
  resolveRequiredCapabilities,
558
686
  runCodexCapabilityPreflight,
687
+ setTomlTableBoolean,
559
688
  };
package/src/codex.lib.mjs CHANGED
@@ -744,8 +744,8 @@ export const executeCodex = async params => {
744
744
  // Issue #1877: deploy the experimental HANDOFF.md Agent Skill so Codex loads
745
745
  // it natively from .agents/skills/handoff/SKILL.md (no-op unless --use-handoff).
746
746
  await deployHandoffSkill({ tempDir, argv, log, $ });
747
-
748
- const capabilityPreflight = await runCodexCapabilityPreflight({ owner, repo, issueNumber, projectDir: tempDir, codexPath, log });
747
+ const codexBaseEnv = getCodexExecEnv(argv.verbose);
748
+ const capabilityPreflight = await runCodexCapabilityPreflight({ owner, repo, issueNumber, projectDir: tempDir, codexPath, log, env: codexBaseEnv });
749
749
  // Execute the Codex command
750
750
  return await executeCodexCommand({
751
751
  tempDir,
@@ -763,7 +763,7 @@ export const executeCodex = async params => {
763
763
  owner,
764
764
  repo,
765
765
  prNumber,
766
- capabilityPreflight,
766
+ capabilityPreflight: { ...capabilityPreflight, codexBaseEnv },
767
767
  });
768
768
  };
769
769
 
@@ -809,7 +809,7 @@ export const executeCodexCommand = async params => {
809
809
  const mappedModel = mapModelToId(argv.model);
810
810
  const { reasoningEffort, source: reasoningEffortSource, rolloutTokenBudget } = resolveCodexReasoningEffort(argv);
811
811
  const isResumeMode = !!argv.resume;
812
- const codexEnv = applyCodexCapabilityEnv(getCodexExecEnv(argv.verbose), {
812
+ const codexEnv = applyCodexCapabilityEnv(capabilityPreflight?.codexBaseEnv || getCodexExecEnv(argv.verbose), {
813
813
  codexHome: capabilityPreflight?.codexHome,
814
814
  baseCodexHome: capabilityPreflight?.baseCodexHome,
815
815
  });