@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 +6 -0
- package/package.json +1 -1
- package/src/codex-capability-preflight.lib.mjs +144 -15
- package/src/codex.lib.mjs +4 -4
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
|
@@ -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(', ')}. ` + `
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
|
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
|
});
|