@link-assistant/hive-mind 2.8.5 → 2.8.7
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,25 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.8.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f8abac0: fix(telegram): apply operator `TELEGRAM_SOLVE_OVERRIDES` to the `/solve` started by `/fix` (#2085)
|
|
8
|
+
|
|
9
|
+
`/fix --ci-cd` genuinely spawns the real `solve.mjs`, but the Telegram bot's
|
|
10
|
+
operator solve overrides (e.g. `--attach-logs`) were only merged into `/solve`
|
|
11
|
+
and `/hive` — never `/fix`. As a result the solve launched by `/fix` ran
|
|
12
|
+
without the operator's defaults. The `mergeArgsWithOverrides` helper is now
|
|
13
|
+
extracted into a shared `src/args-overrides.lib.mjs` module and the `/fix`
|
|
14
|
+
handler applies `solveOverrides` (including an optional `--isolation` override)
|
|
15
|
+
exactly like `/solve` does, restoring the missing defaults.
|
|
16
|
+
|
|
17
|
+
## 2.8.6
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 0140e87: Verify required Codex Agent Skills against the catalog the model actually receives instead of trusting plugin enablement, so a plugin reported as installed but whose skills are invisible is reported with an actionable diagnostic rather than passing the preflight.
|
|
22
|
+
|
|
3
23
|
## 2.8.5
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI-argument override merging (issue #2085).
|
|
3
|
+
*
|
|
4
|
+
* The Telegram bot lets operators configure "override" options that are always
|
|
5
|
+
* applied to a command regardless of what the requester typed — e.g.
|
|
6
|
+
* `TELEGRAM_SOLVE_OVERRIDES="(\n --attach-logs\n --auto-continue\n)"`. These
|
|
7
|
+
* overrides encode the operator's defaults for `/solve`.
|
|
8
|
+
*
|
|
9
|
+
* `mergeArgsWithOverrides` used to live inside `telegram-bot.mjs`, where only
|
|
10
|
+
* the `/solve` and `/hive` handlers could reach it. `/fix` hands its generated
|
|
11
|
+
* issue off to `/solve`, so it must apply the very same solve overrides —
|
|
12
|
+
* otherwise the solve started by `/fix` silently runs without the operator's
|
|
13
|
+
* defaults (issue #2085: "`--attach-logs` were not applied"). Extracting the
|
|
14
|
+
* helper here lets `telegram-fix-command.lib.mjs` reuse the exact same merge
|
|
15
|
+
* semantics without importing the bot entry point (which would be circular).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merge operator override options into a user-supplied argument list.
|
|
20
|
+
*
|
|
21
|
+
* Override flags win: any user flag that also appears in `overrides` (together
|
|
22
|
+
* with its value, if any) is dropped, then every override is appended. Boolean
|
|
23
|
+
* flags and `--flag value` pairs are both handled. The relative order of the
|
|
24
|
+
* surviving user args (including positionals like the issue/repository URL) is
|
|
25
|
+
* preserved, and the overrides are appended at the end so they take precedence
|
|
26
|
+
* in last-wins CLI parsers.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} userArgs - Arguments the requester supplied.
|
|
29
|
+
* @param {string[]} overrides - Operator override options (already tokenized).
|
|
30
|
+
* @returns {string[]} The merged argument list.
|
|
31
|
+
*/
|
|
32
|
+
export function mergeArgsWithOverrides(userArgs, overrides) {
|
|
33
|
+
if (!overrides || overrides.length === 0) {
|
|
34
|
+
return Array.isArray(userArgs) ? userArgs : [];
|
|
35
|
+
}
|
|
36
|
+
const safeUserArgs = Array.isArray(userArgs) ? userArgs : [];
|
|
37
|
+
|
|
38
|
+
// Parse overrides to identify flags and their values
|
|
39
|
+
const overrideFlags = new Map(); // Map of flag -> value (or null for boolean flags)
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < overrides.length; i++) {
|
|
42
|
+
const arg = overrides[i];
|
|
43
|
+
if (arg.startsWith('--')) {
|
|
44
|
+
// Check if next item is a value (doesn't start with --)
|
|
45
|
+
if (i + 1 < overrides.length && !overrides[i + 1].startsWith('--')) {
|
|
46
|
+
overrideFlags.set(arg, overrides[i + 1]);
|
|
47
|
+
i++; // Skip the value in next iteration
|
|
48
|
+
} else {
|
|
49
|
+
overrideFlags.set(arg, null); // Boolean flag
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Filter user args to remove any that conflict with overrides
|
|
55
|
+
const filteredArgs = [];
|
|
56
|
+
for (let i = 0; i < safeUserArgs.length; i++) {
|
|
57
|
+
const arg = safeUserArgs[i];
|
|
58
|
+
if (arg.startsWith('--')) {
|
|
59
|
+
// If this flag exists in overrides, skip it and its value
|
|
60
|
+
if (overrideFlags.has(arg)) {
|
|
61
|
+
// Skip the flag
|
|
62
|
+
// Also skip next arg if it's a value (doesn't start with --)
|
|
63
|
+
if (i + 1 < safeUserArgs.length && !safeUserArgs[i + 1].startsWith('--')) {
|
|
64
|
+
i++; // Skip the value too
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
filteredArgs.push(arg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Merge: filtered user args + overrides
|
|
73
|
+
return [...filteredArgs, ...overrides];
|
|
74
|
+
}
|
|
@@ -155,6 +155,61 @@ const parseJsonCommand = (result, label) => {
|
|
|
155
155
|
|
|
156
156
|
const catalogEntries = catalog => [...(catalog?.installed || []), ...(catalog?.available || [])];
|
|
157
157
|
|
|
158
|
+
// Issue #2084: `codex plugin list` reports enablement, not model visibility.
|
|
159
|
+
//
|
|
160
|
+
// Codex renders the skills the model can actually see into a
|
|
161
|
+
// `<skills_instructions>` block in the prompt. `codex debug prompt-input`
|
|
162
|
+
// prints that prompt, so parsing the block is the only client-side signal that
|
|
163
|
+
// matches what the model receives. Entries are rendered as
|
|
164
|
+
// `- <name>: <description> (file: <path>)`, where `<name>` is bare for skills
|
|
165
|
+
// under `$CODEX_HOME/skills` and `<plugin>:<skill>` for plugin-provided ones.
|
|
166
|
+
const SKILLS_INSTRUCTIONS_BLOCK = /<skills_instructions>([\s\S]*?)<\/skills_instructions>/u;
|
|
167
|
+
const SKILL_CATALOG_ENTRY = /(?:^|\\n)\s*-\s+([a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)\s*:/giu;
|
|
168
|
+
|
|
169
|
+
export const parseModelVisibleSkills = (promptInput = '') => {
|
|
170
|
+
// `codex debug prompt-input` emits JSON, so newlines inside the prompt arrive
|
|
171
|
+
// as the two-character escape `\n`. Match both that and real newlines.
|
|
172
|
+
const block = SKILLS_INSTRUCTIONS_BLOCK.exec(String(promptInput).replace(/\r\n/gu, '\n').replace(/\n/gu, '\\n'));
|
|
173
|
+
if (!block) return null;
|
|
174
|
+
const skills = new Set();
|
|
175
|
+
for (const match of block[1].matchAll(SKILL_CATALOG_ENTRY)) skills.add(match[1].toLowerCase());
|
|
176
|
+
return skills;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Verification is advisory when the probe itself cannot run: an older Codex
|
|
180
|
+
// without `debug prompt-input`, or a sandbox that blocks it, must not fail a
|
|
181
|
+
// run that the previous verification would have allowed.
|
|
182
|
+
const readModelVisibleSkills = async ({ command, env, runCommand, log }) => {
|
|
183
|
+
const result = await runCommand({ command, args: ['debug', 'prompt-input', 'hive-mind capability probe'], env });
|
|
184
|
+
if (result.code !== 0) {
|
|
185
|
+
await log(
|
|
186
|
+
` ⚠️ Could not read the model-visible skill catalog: ${String(result.stderr || result.stdout)
|
|
187
|
+
.trim()
|
|
188
|
+
.slice(0, 200)}`,
|
|
189
|
+
{ verbose: true }
|
|
190
|
+
);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const skills = parseModelVisibleSkills(result.stdout);
|
|
194
|
+
if (!skills) await log(' ⚠️ Codex prompt did not contain a <skills_instructions> block; skipping skill visibility verification', { verbose: true });
|
|
195
|
+
return skills;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const verifyModelVisibleSkills = async ({ command, env, runCommand, log, requiredSkills }) => {
|
|
199
|
+
if (!requiredSkills || requiredSkills.length === 0) return;
|
|
200
|
+
const visible = await readModelVisibleSkills({ command, env, runCommand, log });
|
|
201
|
+
if (!visible) return;
|
|
202
|
+
|
|
203
|
+
await log(` 🔎 Model-visible skills (${visible.size}): ${[...visible].sort().join(', ') || 'none'}`, { verbose: true });
|
|
204
|
+
const invisible = requiredSkills.filter(skill => !visible.has(skill.toLowerCase()));
|
|
205
|
+
if (invisible.length === 0) {
|
|
206
|
+
await log(` ✅ Verified ${requiredSkills.length} required skill(s) are visible to the model`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw new CodexCapabilityPreflightError(`Codex reports the required plugins as installed, but the model cannot see: ${invisible.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].sort().join(', ') || 'none'}.`, { missing: invisible });
|
|
211
|
+
};
|
|
212
|
+
|
|
158
213
|
const skillParts = skill => {
|
|
159
214
|
const separator = skill.indexOf(':');
|
|
160
215
|
return separator === -1 ? { namespace: null, name: skill } : { namespace: skill.slice(0, separator), name: skill.slice(separator + 1) };
|
|
@@ -341,6 +396,7 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
|
|
|
341
396
|
}
|
|
342
397
|
if (plugins.length === 0) {
|
|
343
398
|
await log(' ✅ Required Agent Skills are already available from standard skill directories');
|
|
399
|
+
await verifyModelVisibleSkills({ command, env: baseEnv, runCommand, log, requiredSkills: requirements.skills });
|
|
344
400
|
return { required: true, plugins, skills: requirements.skills, codexHome: null, baseCodexHome };
|
|
345
401
|
}
|
|
346
402
|
|
|
@@ -365,6 +421,13 @@ async function provisionCodexCapabilities({ owner, repo, issueNumber, projectDir
|
|
|
365
421
|
const unverified = plugins.filter(plugin => !verified.has(plugin));
|
|
366
422
|
if (unverified.length > 0) throw new CodexCapabilityPreflightError(`Codex capability installation did not verify successfully: ${unverified.join(', ')}`, { missing: unverified });
|
|
367
423
|
|
|
424
|
+
// Issue #2084: enablement is not exposure. The failing run reached this point
|
|
425
|
+
// with `superpowers@openai-curated` reported as "installed, enabled" while
|
|
426
|
+
// the model saw zero `superpowers:*` skills, so the run proceeded and then
|
|
427
|
+
// stalled on the repository's mandatory preflight. Confirm the requirement
|
|
428
|
+
// against the catalog the model actually receives.
|
|
429
|
+
await verifyModelVisibleSkills({ command, env: scopedEnv, runCommand, log, requiredSkills: requirements.skills });
|
|
430
|
+
|
|
368
431
|
await log(` Codex capability state: ${codexHome}`, { verbose: true });
|
|
369
432
|
return { required: true, plugins, skills: requirements.skills, codexHome, baseCodexHome };
|
|
370
433
|
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -29,6 +29,7 @@ const { createYargsConfig: createHiveYargsConfig } = await import('./hive.config
|
|
|
29
29
|
const { enhanceUnknownArgumentError } = await import('./option-suggestions.lib.mjs');
|
|
30
30
|
const { validateBranchInArgs } = await import('./solve.branch.lib.mjs');
|
|
31
31
|
const { extractIsolationFromArgs, isValidPerCommandIsolation } = await import('./telegram-isolation.lib.mjs');
|
|
32
|
+
const { mergeArgsWithOverrides } = await import('./args-overrides.lib.mjs'); // issue #2085
|
|
32
33
|
|
|
33
34
|
const config = createTelegramYargsConfig(yargs(hideBin(process.argv))).parse();
|
|
34
35
|
|
|
@@ -367,49 +368,6 @@ function validateModelInArgs(args, tool = 'claude') {
|
|
|
367
368
|
return null;
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
function mergeArgsWithOverrides(userArgs, overrides) {
|
|
371
|
-
if (!overrides || overrides.length === 0) {
|
|
372
|
-
return userArgs;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Parse overrides to identify flags and their values
|
|
376
|
-
const overrideFlags = new Map(); // Map of flag -> value (or null for boolean flags)
|
|
377
|
-
|
|
378
|
-
for (let i = 0; i < overrides.length; i++) {
|
|
379
|
-
const arg = overrides[i];
|
|
380
|
-
if (arg.startsWith('--')) {
|
|
381
|
-
// Check if next item is a value (doesn't start with --)
|
|
382
|
-
if (i + 1 < overrides.length && !overrides[i + 1].startsWith('--')) {
|
|
383
|
-
overrideFlags.set(arg, overrides[i + 1]);
|
|
384
|
-
i++; // Skip the value in next iteration
|
|
385
|
-
} else {
|
|
386
|
-
overrideFlags.set(arg, null); // Boolean flag
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Filter user args to remove any that conflict with overrides
|
|
392
|
-
const filteredArgs = [];
|
|
393
|
-
for (let i = 0; i < userArgs.length; i++) {
|
|
394
|
-
const arg = userArgs[i];
|
|
395
|
-
if (arg.startsWith('--')) {
|
|
396
|
-
// If this flag exists in overrides, skip it and its value
|
|
397
|
-
if (overrideFlags.has(arg)) {
|
|
398
|
-
// Skip the flag
|
|
399
|
-
// Also skip next arg if it's a value (doesn't start with --)
|
|
400
|
-
if (i + 1 < userArgs.length && !userArgs[i + 1].startsWith('--')) {
|
|
401
|
-
i++; // Skip the value too
|
|
402
|
-
}
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
filteredArgs.push(arg);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Merge: filtered user args + overrides
|
|
410
|
-
return [...filteredArgs, ...overrides];
|
|
411
|
-
}
|
|
412
|
-
|
|
413
371
|
// Inject --language LOCALE into spawn args if no language flag is already present.
|
|
414
372
|
// Issue #378: telegram bot resolves the user's effective locale and propagates
|
|
415
373
|
// it to spawned solve/hive sessions so the AI tool replies in the same language.
|
|
@@ -595,7 +553,7 @@ registerSubscribeCommands(bot, sharedCommandOpts);
|
|
|
595
553
|
const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
|
|
596
554
|
const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
597
555
|
const { registerFixCommand } = await import('./telegram-fix-command.lib.mjs');
|
|
598
|
-
const { handleFixCommand, FIX_COMMAND_NAMES } = registerFixCommand(bot, { ...sharedCommandOpts, fixEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
|
|
556
|
+
const { handleFixCommand, FIX_COMMAND_NAMES } = registerFixCommand(bot, { ...sharedCommandOpts, fixEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx, solveOverrides });
|
|
599
557
|
const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
|
|
600
558
|
const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
|
|
601
559
|
|
|
@@ -12,6 +12,7 @@ import { validateModelName } from './models/index.mjs';
|
|
|
12
12
|
import { parseFixRepository } from './fix.ci-cd.lib.mjs';
|
|
13
13
|
import { escapeMarkdown } from './telegram-markdown.lib.mjs';
|
|
14
14
|
import { extractIsolationFromArgs, isValidPerCommandIsolation } from './telegram-isolation.lib.mjs';
|
|
15
|
+
import { mergeArgsWithOverrides } from './args-overrides.lib.mjs';
|
|
15
16
|
import { moveArgumentToFront, parseCommandArgs } from './telegram-solve-command.lib.mjs';
|
|
16
17
|
import { formatStartingWorkSessionMessage } from './work-session-formatting.lib.mjs';
|
|
17
18
|
|
|
@@ -87,7 +88,7 @@ function injectLanguageIfMissing(args, locale) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export function registerFixCommand(bot, options) {
|
|
90
|
-
const { VERBOSE, fixEnabled, addBreadcrumb, isOldMessage, isForwardedOrReply, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, resolveLocale = null } = options;
|
|
91
|
+
const { VERBOSE, fixEnabled, addBreadcrumb, isOldMessage, isForwardedOrReply, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, resolveLocale = null, solveOverrides = [] } = options;
|
|
91
92
|
|
|
92
93
|
async function handleFixCommand(ctx) {
|
|
93
94
|
const commandDisplay = '/fix';
|
|
@@ -137,7 +138,22 @@ export function registerFixCommand(bot, options) {
|
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
// Issue #2085: /fix hands the generated issue off to /solve, so it must
|
|
142
|
+
// apply the operator's solve overrides (TELEGRAM_SOLVE_OVERRIDES) exactly
|
|
143
|
+
// like the /solve handler does — otherwise the solve started by /fix runs
|
|
144
|
+
// without the operator's defaults (e.g. --attach-logs). The overrides are
|
|
145
|
+
// forwarded to /solve because /fix passes every option it does not consume
|
|
146
|
+
// through to solve.mjs. An --isolation override applies to the /fix work
|
|
147
|
+
// session itself (which contains the nested /solve), mirroring /solve.
|
|
148
|
+
const { backend: overrideIsolation, filteredArgs: solveOverridesWithoutIsolation } = extractIsolationFromArgs(solveOverrides);
|
|
149
|
+
if (overrideIsolation && !isValidPerCommandIsolation(overrideIsolation)) {
|
|
150
|
+
await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(overrideIsolation)}' in solve overrides. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const effectiveIsolation = overrideIsolation || perCommandIsolation;
|
|
154
|
+
const mergedArgs = mergeArgsWithOverrides(filteredArgs, solveOverridesWithoutIsolation);
|
|
155
|
+
|
|
156
|
+
const modelError = validateFixModel(mergedArgs);
|
|
141
157
|
if (modelError) {
|
|
142
158
|
await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
143
159
|
return;
|
|
@@ -147,12 +163,13 @@ export function registerFixCommand(bot, options) {
|
|
|
147
163
|
const userOptionsRaw = built.args.slice(1).join(' ');
|
|
148
164
|
let infoBlock = `Requested by: ${requester}\nRepository: ${escapeMarkdown(built.repository.url)}`;
|
|
149
165
|
if (userOptionsRaw) infoBlock += `\n\n🛠 Options: ${escapeMarkdown(userOptionsRaw)}`;
|
|
166
|
+
if (solveOverrides.length > 0) infoBlock += `\n\n🔒 Solve overrides: ${escapeMarkdown(solveOverrides.join(' '))}`;
|
|
150
167
|
|
|
151
168
|
const fixUrlContext = { owner: built.repository.owner, repo: built.repository.repo, normalized: built.repository.url };
|
|
152
169
|
const startingMessage = await safeReply(ctx, formatStartingWorkSessionMessage({ infoBlock }), { reply_to_message_id: ctx.message.message_id });
|
|
153
170
|
const fixLocale = resolveLocale ? resolveLocale(ctx) : null;
|
|
154
|
-
const argsForExec = injectLanguageIfMissing(
|
|
155
|
-
await executeAndUpdateMessage(ctx, startingMessage, 'fix', argsForExec, infoBlock,
|
|
171
|
+
const argsForExec = injectLanguageIfMissing(mergedArgs, fixLocale);
|
|
172
|
+
await executeAndUpdateMessage(ctx, startingMessage, 'fix', argsForExec, infoBlock, effectiveIsolation || null, getFixToolFromArgs(argsForExec), fixUrlContext);
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
bot.command(
|