@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- const PLUGIN_SELECTOR = /\b([a-z0-9][a-z0-9-]*@[a-z0-9][a-z0-9-]*(?:-remote)?)\b/gi;
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)) 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);
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 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 () => {} } = {}) {
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 };