@link-assistant/hive-mind 2.8.2 → 2.8.4
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 +28 -0
- package/package.json +1 -1
- package/src/codex-capability-preflight.lib.mjs +108 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.8.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4373bc8: Stop treating JSON Schema, structured-data values, and package selectors as required Codex capabilities while preserving explicit skill and catalog plugin discovery.
|
|
8
|
+
|
|
9
|
+
## 2.8.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 31bd05b: Stop the Codex capability preflight from inventing requirements out of issue prose (#2077).
|
|
14
|
+
|
|
15
|
+
An image-generation issue that asked for `16:9` images aborted the whole run with
|
|
16
|
+
`Required Codex capability unavailable: 16:9`, because the namespaced-skill regex
|
|
17
|
+
accepted any `digits:digits` token and the requirement gate matched the ordinary
|
|
18
|
+
English word "depends".
|
|
19
|
+
|
|
20
|
+
- Capability names must now contain at least one letter, which rejects aspect
|
|
21
|
+
ratios, clock times, host ports, version selectors, currency amounts and email
|
|
22
|
+
addresses while staying compliant with the Agent Skills specification (a
|
|
23
|
+
leading digit remains legal, so `3d-rendering` is still valid).
|
|
24
|
+
- An unresolvable preflight now degrades to a warning and lets Codex run with the
|
|
25
|
+
operator's own capabilities instead of aborting. Set
|
|
26
|
+
`HIVE_MIND_CODEX_CAPABILITY_STRICT=1` to restore the previous fail-fast
|
|
27
|
+
behaviour.
|
|
28
|
+
- `--verbose` now prints the source line behind every accepted and rejected
|
|
29
|
+
capability detection.
|
|
30
|
+
|
|
3
31
|
## 2.8.2
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -16,9 +16,61 @@ import { promisify } from 'node:util';
|
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
17
17
|
const REQUIREMENT_WORDS = /\b(?:depend(?:s|ency)?|install|invoke|mandatory|must|need(?:ed|s)?|preflight|required?|requires|use)\b/i;
|
|
18
18
|
const NEGATED_REQUIREMENT = /\b(?:does\s+not\s+require|not\s+required|optional)\b/i;
|
|
19
|
-
|
|
19
|
+
// `(?!\.[a-z])` keeps email addresses and hostnames (`ops@example.com`) out of
|
|
20
|
+
// the plugin selector space.
|
|
21
|
+
const PLUGIN_SELECTOR = /\b([a-z0-9][a-z0-9-]*@[a-z0-9][a-z0-9-]*(?:-remote)?)\b(?!\.[a-z])/gi;
|
|
20
22
|
const NAMESPACED_SKILL = /\b([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*)\b/gi;
|
|
21
23
|
const EXPLICIT_BARE_SKILL = /\$([a-z0-9][a-z0-9-]*)|`([a-z0-9][a-z0-9-]*)`\s+(?:agent\s+)?skill/gi;
|
|
24
|
+
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))?|use)\s+(?:(?:the|an?)\s+)?(?:(?:agent\s+)?skill\s+)?(?:named\s+)?[`$]?$/i;
|
|
25
|
+
const CAPABILITY_SUFFIX = /^`?\s+(?:agent\s+)?(?:skill|capability)\b/i;
|
|
26
|
+
const STRUCTURED_DATA_VALUES = new Set(['array', 'boolean', 'false', 'integer', 'null', 'number', 'object', 'string', 'true']);
|
|
27
|
+
|
|
28
|
+
// Issue #2077: a capability name must contain at least one letter.
|
|
29
|
+
//
|
|
30
|
+
// The Agent Skills specification (agentskills.io/specification) allows a
|
|
31
|
+
// leading digit — `3d-rendering` is a legal skill name — so requiring a leading
|
|
32
|
+
// letter would be stricter than the spec. Requiring only that *some* letter is
|
|
33
|
+
// present stays spec-compliant while rejecting the purely numeric prose tokens
|
|
34
|
+
// the requirement regexes otherwise capture: aspect ratios (`16:9`), clock
|
|
35
|
+
// times (`9:30`), host ports (`localhost:3000`), version selectors (`node@20`)
|
|
36
|
+
// and currency amounts (`$100`).
|
|
37
|
+
//
|
|
38
|
+
// Charset follows the spec for skills (`a-z`, `0-9`, `-`) plus the underscore
|
|
39
|
+
// that codex-rs `validate_plugin_segment` accepts for plugin and marketplace
|
|
40
|
+
// segments.
|
|
41
|
+
const CAPABILITY_TOKEN = /^(?=[a-z0-9_-]*[a-z])[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/u;
|
|
42
|
+
|
|
43
|
+
// Prose and markdown routinely produce `word:word` and `$word` tokens that are
|
|
44
|
+
// never capability references. Excluding them keeps a heuristic scan of free
|
|
45
|
+
// text from inventing requirements.
|
|
46
|
+
const PROSE_TOKENS = new Set(['agent', 'caution', 'codex', 'default', 'error', 'example', 'file', 'fixme', 'format', 'home', 'http', 'https', 'id', 'important', 'input', 'key', 'line', 'name', 'nb', 'note', 'output', 'path', 'ref', 'required', 'see', 'skill', 'the', 'tip', 'todo', 'type', 'url', 'usage', 'value', 'warning']);
|
|
47
|
+
|
|
48
|
+
const isCapabilityToken = value => CAPABILITY_TOKEN.test(value) && !PROSE_TOKENS.has(value);
|
|
49
|
+
|
|
50
|
+
const hasExplicitCapabilityContext = (line, match) => {
|
|
51
|
+
const before = line.slice(0, match.index);
|
|
52
|
+
const after = line.slice(match.index + match[0].length);
|
|
53
|
+
if (CAPABILITY_SUFFIX.test(after) || before.endsWith('$')) return true;
|
|
54
|
+
const value = match[1].slice(match[1].indexOf(':') + 1).toLowerCase();
|
|
55
|
+
return !STRUCTURED_DATA_VALUES.has(value) && CAPABILITY_PREFIX.test(before);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const hasExplicitPluginContext = (line, match) => {
|
|
59
|
+
const before = line.slice(0, match.index);
|
|
60
|
+
const after = line.slice(match.index + match[0].length);
|
|
61
|
+
const marketplace = match[1].slice(match[1].indexOf('@') + 1);
|
|
62
|
+
return /(?:^|-)(?:bundled|curated|marketplace|remote)(?:-|$)/iu.test(marketplace) || /\bplugin\s+[`$]?$/iu.test(before) || /^`?\s+plugin\b/iu.test(after);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function isCapabilityName(value) {
|
|
66
|
+
const token = String(value || '').toLowerCase();
|
|
67
|
+
const separator = /[:@]/u.exec(token);
|
|
68
|
+
if (!separator) return isCapabilityToken(token);
|
|
69
|
+
const [left, right] = [token.slice(0, separator.index), token.slice(separator.index + 1)];
|
|
70
|
+
// A qualified reference only needs its own halves to be well formed; a prose
|
|
71
|
+
// word such as `note` is meaningless alone but valid as `note:taking`.
|
|
72
|
+
return CAPABILITY_TOKEN.test(left) && CAPABILITY_TOKEN.test(right) && !PROSE_TOKENS.has(left);
|
|
73
|
+
}
|
|
22
74
|
|
|
23
75
|
export class CodexCapabilityPreflightError extends Error {
|
|
24
76
|
constructor(message, details = {}) {
|
|
@@ -38,21 +90,38 @@ export function normalizePluginSelector(selector) {
|
|
|
38
90
|
export function detectRequiredCodexCapabilities(text) {
|
|
39
91
|
const plugins = new Set();
|
|
40
92
|
const skills = new Set();
|
|
93
|
+
// Every accepted capability keeps the line it came from so `--verbose` can
|
|
94
|
+
// explain a detection instead of only reporting its consequence (issue #2077).
|
|
95
|
+
const evidence = [];
|
|
96
|
+
const rejected = [];
|
|
97
|
+
|
|
98
|
+
const accept = (target, value, line) => {
|
|
99
|
+
if (!isCapabilityName(value)) {
|
|
100
|
+
rejected.push({ capability: value, line });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
target.add(value);
|
|
104
|
+
evidence.push({ capability: value, line });
|
|
105
|
+
};
|
|
41
106
|
|
|
42
107
|
for (const rawLine of String(text || '').split(/\r?\n/u)) {
|
|
43
108
|
const line = rawLine.trim();
|
|
44
109
|
if (!line || !REQUIREMENT_WORDS.test(line) || NEGATED_REQUIREMENT.test(line)) continue;
|
|
45
110
|
|
|
46
|
-
for (const match of line.matchAll(PLUGIN_SELECTOR))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const name = (match[1] || match[2]).toLowerCase();
|
|
51
|
-
if (!['agent', 'codex', 'required', 'the'].includes(name)) skills.add(name);
|
|
111
|
+
for (const match of line.matchAll(PLUGIN_SELECTOR)) {
|
|
112
|
+
const selector = normalizePluginSelector(match[1]);
|
|
113
|
+
if (hasExplicitPluginContext(line, match)) accept(plugins, selector, line);
|
|
114
|
+
else rejected.push({ capability: selector, line });
|
|
52
115
|
}
|
|
116
|
+
for (const match of line.matchAll(NAMESPACED_SKILL)) {
|
|
117
|
+
const skill = match[1].toLowerCase();
|
|
118
|
+
if (hasExplicitCapabilityContext(line, match)) accept(skills, skill, line);
|
|
119
|
+
else rejected.push({ capability: skill, line });
|
|
120
|
+
}
|
|
121
|
+
for (const match of line.matchAll(EXPLICIT_BARE_SKILL)) accept(skills, (match[1] || match[2]).toLowerCase(), line);
|
|
53
122
|
}
|
|
54
123
|
|
|
55
|
-
return { plugins: [...plugins].sort(), skills: [...skills].sort() };
|
|
124
|
+
return { plugins: [...plugins].sort(), skills: [...skills].sort(), evidence, rejected };
|
|
56
125
|
}
|
|
57
126
|
|
|
58
127
|
const sanitizePathSegment = value => String(value || '').replace(/[^a-zA-Z0-9._-]/gu, '_');
|
|
@@ -225,7 +294,27 @@ const prepareScopedCodexHome = async ({ baseCodexHome, codexHome }) => {
|
|
|
225
294
|
}
|
|
226
295
|
};
|
|
227
296
|
|
|
228
|
-
export
|
|
297
|
+
export const isCodexCapabilityStrict = (env = process.env) => /^(?:1|true|yes|on)$/iu.test(String(env.HIVE_MIND_CODEX_CAPABILITY_STRICT || ''));
|
|
298
|
+
|
|
299
|
+
export async function runCodexCapabilityPreflight(options = {}) {
|
|
300
|
+
const { log = async () => {}, env = process.env } = options;
|
|
301
|
+
try {
|
|
302
|
+
return await provisionCodexCapabilities(options);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!(error instanceof CodexCapabilityPreflightError)) throw error;
|
|
305
|
+
// Issue #2077: requirements are inferred from free-form issue prose, so a
|
|
306
|
+
// preflight miss is a guess that failed rather than proof the task cannot
|
|
307
|
+
// run. Aborting here discarded an otherwise healthy run because an aspect
|
|
308
|
+
// ratio (`16:9`) was read as a skill name. Degrade to a warning and let
|
|
309
|
+
// Codex execute with the operator's own capabilities.
|
|
310
|
+
if (isCodexCapabilityStrict(env)) throw error;
|
|
311
|
+
await log(`⚠️ Codex capability preflight skipped: ${error.message}`);
|
|
312
|
+
await log(' Continuing with the operator Codex capabilities. Set HIVE_MIND_CODEX_CAPABILITY_STRICT=1 to fail instead.');
|
|
313
|
+
return { required: false, degraded: true, error: error.message, plugins: [], codexHome: null };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
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 () => {} } = {}) {
|
|
229
318
|
if (!owner || !repo || !issueNumber) return { required: false, plugins: [], codexHome: null };
|
|
230
319
|
|
|
231
320
|
// `executeToolWithBun` uses a shell expression for execution. Preflight uses
|
|
@@ -233,14 +322,23 @@ export async function runCodexCapabilityPreflight({ owner, repo, issueNumber, pr
|
|
|
233
322
|
const command = /\s/u.test(codexPath) ? 'codex' : codexPath;
|
|
234
323
|
const requirementText = await readIssueRequirementText({ owner, repo, issueNumber, runCommand });
|
|
235
324
|
const requirements = detectRequiredCodexCapabilities(requirementText);
|
|
325
|
+
for (const { capability, line } of requirements.rejected || []) {
|
|
326
|
+
await log(` ⏭️ Ignored non-capability token '${capability}' from: ${line.slice(0, 160)}`, { verbose: true });
|
|
327
|
+
}
|
|
236
328
|
if (requirements.plugins.length === 0 && requirements.skills.length === 0) return { required: false, plugins: [], codexHome: null };
|
|
237
329
|
|
|
238
330
|
await log(`🔌 Codex capability preflight: detected ${requirements.plugins.length} plugin and ${requirements.skills.length} skill requirement(s)`);
|
|
331
|
+
for (const { capability, line } of requirements.evidence || []) {
|
|
332
|
+
await log(` 🔎 '${capability}' detected from: ${line.slice(0, 160)}`, { verbose: true });
|
|
333
|
+
}
|
|
239
334
|
const baseEnv = { ...process.env, CODEX_HOME: baseCodexHome, HIVE_MIND_PARENT_CODEX_HOME: baseCodexHome };
|
|
240
335
|
const baseCatalogResult = await runCommand({ command, args: ['plugin', 'list', '--available', '--json'], env: baseEnv });
|
|
241
336
|
const baseCatalog = parseJsonCommand(baseCatalogResult, 'Codex plugin catalog discovery');
|
|
242
337
|
const skillDirectories = [path.join(os.homedir(), '.agents', 'skills'), projectDir && path.join(projectDir, '.agents', 'skills')].filter(Boolean);
|
|
243
338
|
const plugins = await resolveRequiredPlugins({ requirements, catalog: baseCatalog, skillDirectories });
|
|
339
|
+
for (const plugin of plugins) {
|
|
340
|
+
await log(` ✅ Verified ${plugin} in the Codex plugin catalog`, { verbose: true });
|
|
341
|
+
}
|
|
244
342
|
if (plugins.length === 0) {
|
|
245
343
|
await log(' ✅ Required Agent Skills are already available from standard skill directories');
|
|
246
344
|
return { required: true, plugins, skills: requirements.skills, codexHome: null, baseCodexHome };
|
|
@@ -271,4 +369,4 @@ export async function runCodexCapabilityPreflight({ owner, repo, issueNumber, pr
|
|
|
271
369
|
return { required: true, plugins, skills: requirements.skills, codexHome, baseCodexHome };
|
|
272
370
|
}
|
|
273
371
|
|
|
274
|
-
export default { applyCodexCapabilityEnv, detectRequiredCodexCapabilities, runCodexCapabilityPreflight };
|
|
372
|
+
export default { applyCodexCapabilityEnv, detectRequiredCodexCapabilities, isCapabilityName, runCodexCapabilityPreflight };
|