@link-assistant/hive-mind 1.56.3 → 1.56.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,11 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.56.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 2d6d405: Fix Telegram bot LINO configuration parsing for parenthesized option/value links such as `(--isolation screen)`.
8
+
3
9
  ## 1.56.3
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": "1.56.3",
3
+ "version": "1.56.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",
@@ -15,7 +15,7 @@
15
15
  "hive-telegram-bot": "./src/telegram-bot.mjs"
16
16
  },
17
17
  "scripts": {
18
- "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
18
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
19
19
  "test:queue": "node tests/solve-queue.test.mjs",
20
20
  "test:limits-display": "node tests/limits-display.test.mjs",
21
21
  "test:usage-limit": "node tests/test-usage-limit.mjs",
@@ -38,6 +38,43 @@ const LinoParser = linoModule.Parser || linoModule.default?.Parser;
38
38
 
39
39
  const fs = await import('fs');
40
40
 
41
+ function isCliOptionToken(value) {
42
+ return /^--[a-zA-Z0-9][a-zA-Z0-9=_.-]*$/.test(value) || /^-[a-zA-Z]$/.test(value);
43
+ }
44
+
45
+ function collectStringValues(value, result = []) {
46
+ if (value && typeof value === 'object' && Array.isArray(value.values)) {
47
+ if (value.id !== null && value.id !== undefined) {
48
+ result.push(String(value.id));
49
+ }
50
+ for (const child of value.values) {
51
+ collectStringValues(child, result);
52
+ }
53
+ } else if (value !== null && value !== undefined) {
54
+ result.push(String(value));
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function validateNoBareSameLineOptions(content) {
60
+ let currentVar = 'configuration';
61
+
62
+ for (const line of content.split(/\r?\n/)) {
63
+ const trimmed = line.trim();
64
+ if (!trimmed || trimmed === '(' || trimmed === ')') continue;
65
+
66
+ const topLevelMatch = !/^\s/.test(line) ? trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*):(?:\s*(.*))?$/) : null;
67
+ const valueText = topLevelMatch ? (topLevelMatch[2] || '').trim() : trimmed;
68
+ if (topLevelMatch) currentVar = topLevelMatch[1];
69
+ if (!valueText || valueText === '(' || valueText === ')' || valueText.startsWith('(')) continue;
70
+
71
+ const parts = valueText.split(/\s+/).filter(Boolean);
72
+ if (parts.length > 1 && isCliOptionToken(parts[0])) {
73
+ throw new Error(`Invalid LINO format in "${currentVar}": Multiple values on the same line are not supported.\n` + `Found: "${parts.join(' ')}"\n` + `Each value must be on its own line with proper indentation, or grouped explicitly as a parenthesized link.`);
74
+ }
75
+ }
76
+ }
77
+
41
78
  /**
42
79
  * LenvReader - Reads and parses .lenv files using LINO notation
43
80
  */
@@ -59,6 +96,8 @@ export class LenvReader {
59
96
  const result = {};
60
97
 
61
98
  try {
99
+ validateNoBareSameLineOptions(content);
100
+
62
101
  // Parse the entire content as LINO
63
102
  const parsed = this.parser.parse(content);
64
103
 
@@ -77,36 +116,9 @@ export class LenvReader {
77
116
 
78
117
  // The values are the variable value
79
118
  if (link.values && link.values.length > 0) {
80
- // Check for nested structures (multiple items on same line) - reject with error
81
- // A nested tuple with id=null that appears amongst other direct values indicates
82
- // same-line grouping (e.g., "--option1 --option2" on same line)
83
- // However, if the entire list is a SINGLE nested tuple (e.g., "VAR: (\n 1\n 2\n)"),
84
- // that's valid parenthesized syntax
85
- const hasDirectValues = link.values.some(v => v && typeof v === 'object' && v.id !== null);
86
- const hasNestedTuples = link.values.some(v => v && typeof v === 'object' && v.id === null && v.values && v.values.length > 0);
87
-
88
- if (hasDirectValues && hasNestedTuples) {
89
- // Mixed direct values and nested tuples indicates same-line grouping
90
- for (const v of link.values) {
91
- if (v && typeof v === 'object' && v.id === null && v.values && v.values.length > 0) {
92
- const nestedItems = v.values.map(nested => nested.id || nested).join(' ');
93
- throw new Error(`Invalid LINO format in "${varName}": Multiple values on the same line are not supported.\n` + `Found: "${nestedItems}"\n` + `Each value must be on its own line with proper indentation.`);
94
- }
95
- }
96
- }
97
-
98
- // Determine which values to validate for invalid characters
99
- // If it's a single nested tuple (parenthesized list), unwrap it for validation
100
- let valuesToValidate = link.values;
101
- if (link.values.length === 1 && link.values[0] && typeof link.values[0] === 'object' && link.values[0].id === null && link.values[0].values) {
102
- // Single parenthesized list - use inner values
103
- valuesToValidate = link.values[0].values;
104
- }
105
-
106
119
  // Check for invalid characters in option-like values
107
- for (const v of valuesToValidate) {
120
+ for (const valueStr of link.values.flatMap(v => collectStringValues(v))) {
108
121
  // Options should match pattern: --option-name or -o (with optional =value)
109
- const valueStr = v.id || v;
110
122
  if (typeof valueStr === 'string' && valueStr.startsWith('-')) {
111
123
  // This looks like a command-line option, validate it
112
124
  // Valid option pattern: -x, --option-name, --option-name=value
@@ -119,7 +131,7 @@ export class LenvReader {
119
131
  }
120
132
 
121
133
  // If there are multiple values, format them as LINO notation
122
- const values = link.values.map(v => v.id || v);
134
+ const values = link.values.flatMap(v => collectStringValues(v));
123
135
 
124
136
  // If it's a single value, just use it as-is
125
137
  if (values.length === 1) {
package/src/lino.lib.mjs CHANGED
@@ -9,6 +9,20 @@ const fs = await import('fs');
9
9
  const path = await import('path');
10
10
  const os = await import('os');
11
11
 
12
+ function collectStringValues(value, result = []) {
13
+ if (value && typeof value === 'object' && Array.isArray(value.values)) {
14
+ if (value.id !== null && value.id !== undefined) {
15
+ result.push(String(value.id));
16
+ }
17
+ for (const child of value.values) {
18
+ collectStringValues(child, result);
19
+ }
20
+ } else if (value !== null && value !== undefined) {
21
+ result.push(String(value));
22
+ }
23
+ return result;
24
+ }
25
+
12
26
  export class LinksNotationManager {
13
27
  constructor() {
14
28
  this.parser = new LinoParser();
@@ -26,8 +40,7 @@ export class LinksNotationManager {
26
40
 
27
41
  if (link.values && link.values.length > 0) {
28
42
  for (const value of link.values) {
29
- const val = value.id || value;
30
- values.push(val);
43
+ values.push(...collectStringValues(value));
31
44
  }
32
45
  } else if (link.id) {
33
46
  values.push(link.id);
@@ -50,9 +63,11 @@ export class LinksNotationManager {
50
63
 
51
64
  if (link.values && link.values.length > 0) {
52
65
  for (const value of link.values) {
53
- const num = parseInt(value.id || value);
54
- if (!isNaN(num)) {
55
- ids.push(num);
66
+ for (const linkValue of collectStringValues(value)) {
67
+ const num = parseInt(linkValue);
68
+ if (!isNaN(num)) {
69
+ ids.push(num);
70
+ }
56
71
  }
57
72
  }
58
73
  } else if (link.id) {
@@ -79,8 +94,7 @@ export class LinksNotationManager {
79
94
 
80
95
  if (link.values && link.values.length > 0) {
81
96
  for (const value of link.values) {
82
- const linkStr = value.id || value;
83
- if (typeof linkStr === 'string') {
97
+ for (const linkStr of collectStringValues(value)) {
84
98
  links.push(linkStr);
85
99
  }
86
100
  }
@@ -28,7 +28,7 @@ const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.
28
28
 
29
29
  // Load .env/.lenv configuration (issue #1318)
30
30
  dotenvx.config({ quiet: true, ignore: ['MISSING_ENV_FILE'] });
31
- loadLenvConfig({ override: true, quiet: true });
31
+ await loadLenvConfig({ override: true, quiet: true });
32
32
 
33
33
  const yargsModule = await use('yargs@17.7.2');
34
34
  const yargs = yargsModule.default || yargsModule;
@@ -123,7 +123,7 @@ const config = yargs(hideBin(process.argv))
123
123
 
124
124
  // Configuration priority: CLI option > --configuration LINO > .lenv > .env
125
125
  if (config.configuration) {
126
- loadLenvConfig({ configuration: config.configuration, override: true, quiet: true });
126
+ await loadLenvConfig({ configuration: config.configuration, override: true, quiet: true });
127
127
  }
128
128
 
129
129
  const BOT_TOKEN = config.token || getenv('TELEGRAM_BOT_TOKEN', '');
@@ -173,8 +173,12 @@ if (ISOLATION_BACKEND) {
173
173
  if (solveEnabled && solveOverrides.length > 0) {
174
174
  console.log('Validating solve overrides...');
175
175
  try {
176
+ const { backend: solveOverrideIsolation, filteredArgs: solveOverridesForValidation } = extractIsolationFromArgs(solveOverrides);
177
+ if (solveOverrideIsolation && !isValidPerCommandIsolation(solveOverrideIsolation)) {
178
+ throw new Error(`Invalid --isolation value '${solveOverrideIsolation}'. Must be: screen, tmux, or docker`);
179
+ }
176
180
  // Add a dummy URL as the first argument (required positional for solve)
177
- const testArgs = ['https://github.com/test/test/issues/1', ...solveOverrides];
181
+ const testArgs = ['https://github.com/test/test/issues/1', ...solveOverridesForValidation];
178
182
 
179
183
  // Temporarily suppress stderr to avoid yargs error output during validation
180
184
  const originalStderrWrite = process.stderr.write;
@@ -197,7 +201,7 @@ if (solveEnabled && solveOverrides.length > 0) {
197
201
  });
198
202
  await testYargs.parse(testArgs);
199
203
  // Issue #1482: Validate --base-branch in overrides early
200
- const overrideBranchError = validateBranchInArgs(solveOverrides);
204
+ const overrideBranchError = validateBranchInArgs(solveOverridesForValidation);
201
205
  if (overrideBranchError) throw new Error(overrideBranchError);
202
206
  console.log('✅ Solve overrides validated successfully');
203
207
  } finally {
@@ -216,8 +220,12 @@ if (solveEnabled && solveOverrides.length > 0) {
216
220
  if (hiveEnabled && hiveOverrides.length > 0) {
217
221
  console.log('Validating hive overrides...');
218
222
  try {
223
+ const { backend: hiveOverrideIsolation, filteredArgs: hiveOverridesForValidation } = extractIsolationFromArgs(hiveOverrides);
224
+ if (hiveOverrideIsolation && !isValidPerCommandIsolation(hiveOverrideIsolation)) {
225
+ throw new Error(`Invalid --isolation value '${hiveOverrideIsolation}'. Must be: screen, tmux, or docker`);
226
+ }
219
227
  // Add a dummy URL as the first argument (required positional for hive)
220
- const testArgs = ['https://github.com/test/test', ...hiveOverrides];
228
+ const testArgs = ['https://github.com/test/test', ...hiveOverridesForValidation];
221
229
 
222
230
  // Temporarily suppress stderr to avoid yargs error output during validation
223
231
  const originalStderrWrite = process.stderr.write;
@@ -239,7 +247,7 @@ if (hiveEnabled && hiveOverrides.length > 0) {
239
247
  throw new Error(msg);
240
248
  });
241
249
  await testYargs.parse(testArgs);
242
- const overrideBranchError = validateBranchInArgs(hiveOverrides); // Issue #1482
250
+ const overrideBranchError = validateBranchInArgs(hiveOverridesForValidation); // Issue #1482
243
251
  if (overrideBranchError) throw new Error(overrideBranchError);
244
252
  console.log('✅ Hive overrides validated successfully');
245
253
  } finally {
@@ -890,7 +898,13 @@ async function handleSolveCommand(ctx) {
890
898
  await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(solvePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
891
899
  return;
892
900
  }
893
- const args = mergeArgsWithOverrides(userArgsWithoutIsolation, solveOverrides);
901
+ const mergedSolveArgs = mergeArgsWithOverrides(userArgsWithoutIsolation, solveOverrides);
902
+ const { backend: solveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedSolveArgs);
903
+ if (solveOverrideIsolation && !isValidPerCommandIsolation(solveOverrideIsolation)) {
904
+ await safeReply(ctx, `❌ Invalid locked --isolation value '${escapeMarkdown(solveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
905
+ return;
906
+ }
907
+ const effectiveSolveIsolation = solveOverrideIsolation || solvePerCommandIsolation;
894
908
 
895
909
  // Determine tool from args (default: claude)
896
910
  let solveTool = 'claude';
@@ -990,9 +1004,9 @@ async function handleSolveCommand(ctx) {
990
1004
  const toolQueuedCount = queueStats.queuedByTool[solveTool] || 0; // tool-specific queue count (#1551)
991
1005
  if (check.canStart && toolQueuedCount === 0) {
992
1006
  const startingMessage = await safeReply(ctx, `🚀 Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
993
- await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, solvePerCommandIsolation);
1007
+ await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock, effectiveSolveIsolation);
994
1008
  } else {
995
- const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: solvePerCommandIsolation });
1009
+ const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool, perCommandIsolation: effectiveSolveIsolation });
996
1010
  let queueMessage = `📋 Solve command queued (${solveTool} queue position #${toolQueuedCount + 1})\n\n${infoBlock}`; // tool-specific position (#1551)
997
1011
  if (check.reason) queueMessage += `\n\n⏳ Waiting: ${escapeMarkdown(check.reason)}`;
998
1012
  const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
@@ -1102,7 +1116,13 @@ async function handleHiveCommand(ctx) {
1102
1116
  await safeReply(ctx, `❌ Invalid --isolation value '${escapeMarkdown(hivePerCommandIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1103
1117
  return;
1104
1118
  }
1105
- const args = mergeArgsWithOverrides(normalizedArgsWithoutIsolation, hiveOverrides);
1119
+ const mergedHiveArgs = mergeArgsWithOverrides(normalizedArgsWithoutIsolation, hiveOverrides);
1120
+ const { backend: hiveOverrideIsolation, filteredArgs: args } = extractIsolationFromArgs(mergedHiveArgs);
1121
+ if (hiveOverrideIsolation && !isValidPerCommandIsolation(hiveOverrideIsolation)) {
1122
+ await safeReply(ctx, `❌ Invalid locked --isolation value '${escapeMarkdown(hiveOverrideIsolation)}'. Must be: screen, tmux, or docker`, { reply_to_message_id: ctx.message.message_id });
1123
+ return;
1124
+ }
1125
+ const effectiveHiveIsolation = hiveOverrideIsolation || hivePerCommandIsolation;
1106
1126
 
1107
1127
  // Determine tool from args (default: claude)
1108
1128
  let hiveTool = 'claude';
@@ -1161,7 +1181,7 @@ async function handleHiveCommand(ctx) {
1161
1181
  }
1162
1182
 
1163
1183
  const startingMessage = await safeReply(ctx, `🚀 Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
1164
- await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, hivePerCommandIsolation);
1184
+ await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock, effectiveHiveIsolation);
1165
1185
  }
1166
1186
 
1167
1187
  bot.command(/^hive$/i, handleHiveCommand);