@link-assistant/hive-mind 2.11.13 → 2.12.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -1
  3. package/src/agent-command.lib.mjs +74 -0
  4. package/src/agent.lib.mjs +59 -34
  5. package/src/agentic-cli-updater.lib.mjs +241 -0
  6. package/src/claude.connection.lib.mjs +209 -0
  7. package/src/claude.lib.mjs +6 -202
  8. package/src/codex.lib.mjs +0 -128
  9. package/src/formal-ai-isolation.lib.mjs +62 -0
  10. package/src/formal-ai-maintenance.lib.mjs +106 -0
  11. package/src/formal-ai-model.lib.mjs +25 -0
  12. package/src/formal-ai-runtime.lib.mjs +10 -0
  13. package/src/formal-ai-sidecar.lib.mjs +565 -0
  14. package/src/formal-ai-updater.lib.mjs +294 -0
  15. package/src/formal-ai-version.lib.mjs +100 -0
  16. package/src/formal-ai.lib.mjs +11 -16
  17. package/src/github-rate-limit.lib.mjs +3 -0
  18. package/src/github-url-parser.lib.mjs +255 -0
  19. package/src/github.lib.mjs +22 -343
  20. package/src/hive.mjs +0 -152
  21. package/src/interactive-mode.lib.mjs +0 -43
  22. package/src/isolation-runner.lib.mjs +44 -173
  23. package/src/limits.lib.mjs +0 -89
  24. package/src/model-args.lib.mjs +32 -0
  25. package/src/models/index.mjs +5 -19
  26. package/src/session-monitor.lib.mjs +14 -172
  27. package/src/solve.auto-merge.lib.mjs +70 -164
  28. package/src/solve.mjs +31 -193
  29. package/src/solve.repository.lib.mjs +0 -83
  30. package/src/solve.results.lib.mjs +2 -92
  31. package/src/solve.session.lib.mjs +52 -19
  32. package/src/solve.tool-uncommitted.lib.mjs +22 -0
  33. package/src/state-lock.lib.mjs +82 -0
  34. package/src/telegram-bot.mjs +17 -65
  35. package/src/telegram-fix-command.lib.mjs +1 -8
  36. package/src/telegram-merge-queue.lib.mjs +3 -155
  37. package/src/telegram-solve-queue.lib.mjs +9 -168
  38. package/src/telegram-task-command.lib.mjs +1 -8
  39. package/src/use-m-bootstrap.lib.mjs +6 -5
  40. package/src/use-with-retry.lib.mjs +128 -2
  41. package/src/working-session-summary.lib.mjs +47 -1
@@ -1,6 +1,7 @@
1
1
  import { buildUserMention } from './buildUserMention.lib.mjs';
2
2
  import { validateModelName } from './models/index.mjs';
3
3
  import { getLinoYargsFactory } from './cli-arguments.lib.mjs';
4
+ import { getModelFromArgs } from './model-args.lib.mjs';
4
5
  import { createYargsConfig as createTaskYargsConfig } from './task.config.lib.mjs';
5
6
  import { createCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
6
7
  import { parseFixRepository } from './fix.ci-cd.lib.mjs';
@@ -47,14 +48,6 @@ export function getTaskToolFromArgs(args) {
47
48
  return 'claude';
48
49
  }
49
50
 
50
- function getModelFromArgs(args) {
51
- for (let i = 0; i < args.length; i++) {
52
- if ((args[i] === '--model' || args[i] === '-m') && i + 1 < args.length) return args[i + 1];
53
- if (args[i].startsWith('--model=')) return args[i].substring('--model='.length);
54
- }
55
- return null;
56
- }
57
-
58
51
  function validateTaskModel(args) {
59
52
  const model = getModelFromArgs(args);
60
53
  if (!model) return null;
@@ -3,9 +3,10 @@
3
3
  import { wrapUseWithRetry } from './use-with-retry.lib.mjs';
4
4
  import { wrapUseWithSingleFlight } from './use-m-single-flight.lib.mjs';
5
5
 
6
- export const USE_M_BOOTSTRAP_URL = 'https://unpkg.com/use-m/use.js';
7
- // Issue #2113: the fallback is only reached when unpkg cannot serve the `latest`
8
- // bundle, but until now it pinned 8.13.8 — the last release *without* any
6
+ // Both URLs are immutable and use independent CDNs. A mutable `latest` URL
7
+ // allowed an upstream publication to change unchanged CI runs (issue #2150).
8
+ export const USE_M_BOOTSTRAP_URL = 'https://unpkg.com/use-m@8.15.0/use.js';
9
+ // Issue #2113: the fallback previously pinned 8.13.8 — the last release *without* any
9
10
  // corrupt-alias self-healing. A CDN hiccup therefore silently downgraded every
10
11
  // dependency import to the least resilient loader available. 8.14.4 is the first
11
12
  // release that both repairs corrupt aliases (8.14.3, use-m #66/#67) and removes
@@ -13,10 +14,10 @@ export const USE_M_BOOTSTRAP_URL = 'https://unpkg.com/use-m/use.js';
13
14
  // upstream recovery instead of losing it. 8.15.0 (use-m #70, the report filed
14
15
  // from this issue) additionally serialises installs of one alias across
15
16
  // processes with its own `.use-m/<alias>.lock` plus a post-install marker, so
16
- // the pinned fallback now carries upstream prevention too — verified with the
17
+ // the pinned bootstraps now carry upstream prevention too — verified with the
17
18
  // standalone reproduction: 8.14.4 fails 22/24 concurrent loads, 8.15.0 fails
18
19
  // 0/24 (docs/case-studies/issue-2113/raw/experiment-upstream-use-m-8.15.0-fixed.log).
19
- export const USE_M_BOOTSTRAP_FALLBACK_URL = 'https://unpkg.com/use-m@8.15.0/use.js';
20
+ export const USE_M_BOOTSTRAP_FALLBACK_URL = 'https://cdn.jsdelivr.net/npm/use-m@8.15.0/use.js';
20
21
 
21
22
  const isMissingUseMBundle = code => /^Not found: \/use-m@[^/]+\/use\.js\s*$/.test(code.trim());
22
23
 
@@ -31,6 +31,49 @@
31
31
  const ALIAS_CLEANUP_ERROR = /^Failed to remove (?:corrupt|incomplete) npm alias '([^']+)'\.$/;
32
32
  const RETRYABLE_RM_CODES = new Set(['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']);
33
33
 
34
+ // `use-m` otherwise resolves every bare specifier through npm's mutable
35
+ // `latest` tag at runtime. command-stream@0.19.0 changing its CommonJS entry
36
+ // point broke unchanged Hive Mind commits in issue #2150. Keep every runtime
37
+ // dependency reproducible; explicit versions and subpaths remain untouched.
38
+ export const USE_M_PACKAGE_VERSIONS = Object.freeze({
39
+ '@dotenvx/dotenvx': '2.21.0',
40
+ 'command-stream': '0.18.0',
41
+ getenv: '2.0.0',
42
+ 'links-notation': '0.13.0',
43
+ 'lino-arguments': '0.3.0',
44
+ telegraf: '4.16.3',
45
+ yargs: '17.7.2',
46
+ zx: '8.8.5',
47
+ });
48
+
49
+ export const pinUseMSpecifier = specifier => {
50
+ const version = USE_M_PACKAGE_VERSIONS[specifier];
51
+ return version ? `${specifier}@${version}` : specifier;
52
+ };
53
+
54
+ /**
55
+ * Undo Node 24's CommonJS namespace wrapper when `use-m` returns it verbatim.
56
+ *
57
+ * Node 23+ exposes `module.exports` as a synthetic named export alongside the
58
+ * default export. use-m@8.15.0 does not classify that key as namespace
59
+ * metadata, so a CommonJS package with no real named exports is returned as an
60
+ * object instead of its `module.exports` value. Requiring object identity keeps
61
+ * real ESM namespaces and unusual hybrid modules intact.
62
+ *
63
+ * @param {unknown} loaded
64
+ * @param {object} [options]
65
+ * @param {string} [options.specifier]
66
+ * @param {(message: string) => void} [options.log]
67
+ * @returns {unknown}
68
+ */
69
+ export const normalizeCommonJsNamespace = (loaded, options = {}) => {
70
+ if (loaded && typeof loaded === 'object' && Object.hasOwn(loaded, 'default') && Object.hasOwn(loaded, 'module.exports') && loaded.default === loaded['module.exports']) {
71
+ options.log?.(`use('${options.specifier ?? 'unknown'}') normalized Node's CommonJS namespace marker`);
72
+ return loaded.default;
73
+ }
74
+ return loaded;
75
+ };
76
+
34
77
  /**
35
78
  * @param {(specifier: string) => Promise<unknown>} use - the use-m loader.
36
79
  * @param {string} specifier - the npm specifier to load (e.g. `'getenv'`).
@@ -46,18 +89,23 @@ const RETRYABLE_RM_CODES = new Set(['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'E
46
89
  * @returns {Promise<unknown>} the module returned by use-m.
47
90
  */
48
91
  export const useWithRetry = async (use, specifier, options = {}) => {
92
+ const requestedSpecifier = specifier;
93
+ specifier = pinUseMSpecifier(specifier);
49
94
  const attempts = options.attempts ?? 3;
50
95
  const cleanup = options.cleanup ?? defaultCleanup;
51
96
  const sleep = options.sleep ?? defaultSleep;
52
97
  const backoffMs = options.backoffMs ?? 1000;
53
98
  const log = options.log ?? defaultLog;
54
99
  const importModule = options.importModule ?? defaultImport;
100
+ const installWithoutBinLinks = options.installWithoutBinLinks ?? defaultInstallWithoutBinLinks;
55
101
  const extraArgs = options.args ?? [];
102
+ if (requestedSpecifier !== specifier) log(`use('${requestedSpecifier}') pinned to '${specifier}'`);
56
103
  let lastError;
57
104
  let cleanedImportPath = null;
58
105
  for (let attempt = 1; attempt <= attempts; attempt++) {
59
106
  try {
60
- return await use(specifier, ...extraArgs);
107
+ const loaded = await use(specifier, ...extraArgs);
108
+ return normalizeCommonJsNamespace(loaded, { specifier, log });
61
109
  } catch (error) {
62
110
  lastError = error;
63
111
  // Node's ESM loader caches *failed* module evaluations by resolved URL.
@@ -71,7 +119,7 @@ export const useWithRetry = async (use, specifier, options = {}) => {
71
119
  try {
72
120
  const recovered = await importModule(cleanedImportPath, attempt);
73
121
  log(`use('${specifier}') recovered via a cache-busted import of ${cleanedImportPath}`);
74
- return recovered;
122
+ return normalizeCommonJsNamespace(recovered, { specifier, log });
75
123
  } catch (reimportError) {
76
124
  log(`cache-busted import of ${cleanedImportPath} also failed: ${reimportError?.message}`);
77
125
  }
@@ -82,6 +130,21 @@ export const useWithRetry = async (use, specifier, options = {}) => {
82
130
  throw error;
83
131
  }
84
132
  log(`use('${specifier}') failed on attempt ${attempt}/${attempts}: ${error?.message} — retrying`);
133
+ // npm gives every global alias the package's original executable names.
134
+ // When a pinned alias replaces use-m's former `-v-latest` alias, packages
135
+ // such as zx collide on `/bin/zx` even though both module directories can
136
+ // coexist. use-m only imports the package and does not need its CLI link,
137
+ // so finish the pinned install without bin links and retry the import.
138
+ if (requestedSpecifier !== specifier && isBinLinkInstallConflict(error)) {
139
+ const details = extractFailedInstallDetails(error);
140
+ try {
141
+ await installWithoutBinLinks({ specifier, globalRoot: details?.globalRoot, error });
142
+ log(`use('${specifier}') repaired npm's alias bin-link collision`);
143
+ continue;
144
+ } catch (repairError) {
145
+ log(`use('${specifier}') could not repair npm's alias bin-link collision: ${repairError?.message}`);
146
+ }
147
+ }
85
148
  // Mode 4 (issue #2092): `npm install -g` itself failed (network blip,
86
149
  // registry 5xx, DinD DNS not up yet). There is nothing to delete; just
87
150
  // back off and let npm try again.
@@ -127,6 +190,69 @@ export const isTransientInstallError = error => {
127
190
  return /^Failed to install .+ globally into /.test(message);
128
191
  };
129
192
 
193
+ export const extractFailedInstallDetails = error => {
194
+ const message = typeof error?.message === 'string' ? error.message : '';
195
+ const match = message.match(/^Failed to install (.+?) globally into '([^']+)'(?:\.| after\b)/);
196
+ return match ? { specifier: match[1], globalRoot: match[2] } : null;
197
+ };
198
+
199
+ export const isBinLinkInstallConflict = error => {
200
+ if (!extractFailedInstallDetails(error)) return false;
201
+ // use-m may aggregate all npm attempts into the outer message instead of
202
+ // preserving stderr on the final cause, so inspect both levels.
203
+ const causeText = [error?.message, error?.cause?.message, error?.cause?.stderr, error?.cause?.stdout, error?.cause?.cause?.message, error?.cause?.cause?.stderr].filter(Boolean).join('\n');
204
+ return /\bEEXIST\b/.test(causeText) && /(?:File exists|file already exists|\/bin\/|\\bin\\)/i.test(causeText);
205
+ };
206
+
207
+ const installErrorText = error => [error?.message, error?.stderr, error?.stdout, error?.cause?.message, error?.cause?.stderr, error?.cause?.stdout, error?.cause?.cause?.message, error?.cause?.cause?.stderr].filter(Boolean).join('\n');
208
+
209
+ export const extractConflictingBinPath = error => {
210
+ const match = installErrorText(error).match(/(?:^|\n)npm error path ([^\r\n]+)/);
211
+ return match?.[1]?.trim() ?? null;
212
+ };
213
+
214
+ export const aliasForUseMSpecifier = specifier => {
215
+ const match = specifier.match(/^(@[^/]+\/[^@/]+|[^@/]+)@(.+)$/);
216
+ if (!match) throw new Error(`Expected an exact npm package specifier, got '${specifier}'`);
217
+ const [, packageName, version] = match;
218
+ return `${packageName.replace('@', '').replace('/', '-')}-v-${version}`;
219
+ };
220
+
221
+ export const npmPrefixForGlobalRoot = globalRoot => {
222
+ if (!globalRoot) return null;
223
+ const normalized = globalRoot.replaceAll('\\', '/').replace(/\/$/, '');
224
+ if (normalized.endsWith('/lib/node_modules')) return normalized.slice(0, -'/lib/node_modules'.length);
225
+ if (normalized.endsWith('/node_modules')) return normalized.slice(0, -'/node_modules'.length);
226
+ return null;
227
+ };
228
+
229
+ export const installAliasWithoutBinLinks = async ({ specifier, globalRoot, error, runner, readlink } = {}) => {
230
+ const alias = aliasForUseMSpecifier(specifier);
231
+ const prefix = npmPrefixForGlobalRoot(globalRoot);
232
+ const binPath = extractConflictingBinPath(error);
233
+ if (!prefix || !binPath) throw new Error('Cannot safely identify the conflicting use-m package binary');
234
+ const { dirname, resolve, sep } = await import('node:path');
235
+ const readSymbolicLink = readlink ?? (await import('node:fs/promises')).readlink;
236
+ const target = resolve(dirname(binPath), await readSymbolicLink(binPath));
237
+ const [packageName] = specifier.match(/^(@[^/]+\/[^@/]+|[^@/]+)@(.+)$/)?.slice(1) ?? [];
238
+ const aliasPrefix = `${packageName?.replace('@', '').replace('/', '-')}-v-`;
239
+ const normalizedRoot = resolve(globalRoot);
240
+ if (!packageName || !target.startsWith(`${normalizedRoot}${sep}${aliasPrefix}`)) {
241
+ throw new Error(`Refusing to replace ${binPath}; it is not owned by a use-m alias for ${packageName ?? specifier}`);
242
+ }
243
+ // npm 11 still checks an existing global executable even with
244
+ // --no-bin-links. --force is safe here only because the symlink target was
245
+ // verified above as another version alias managed by use-m for this package.
246
+ const args = ['install', '-g', '--force', '--no-bin-links', `${alias}@npm:${specifier}`];
247
+ if (prefix) args.push('--prefix', prefix);
248
+ if (runner) return runner('npm', args);
249
+ const { execFile } = await import('node:child_process');
250
+ const { promisify } = await import('node:util');
251
+ return promisify(execFile)('npm', args);
252
+ };
253
+
254
+ const defaultInstallWithoutBinLinks = installAliasWithoutBinLinks;
255
+
130
256
  export const isCorruptInstallError = error => {
131
257
  const cause = error?.cause;
132
258
  if (cause instanceof SyntaxError) return true;
@@ -49,6 +49,52 @@ export const redactWorkspacePaths = text => {
49
49
  return text.replace(WORKSPACE_PATH_PATTERN, WORKSPACE_PATH_PLACEHOLDER);
50
50
  };
51
51
 
52
+ /**
53
+ * Fence structured text emitted after a Markdown lead-in ending in `:`.
54
+ * Formal AI plan events use a root record followed by space/tab-indented Lino;
55
+ * GitHub otherwise collapses that layout into ordinary prose. Existing fences
56
+ * and ordinary Markdown are preserved, making this safe to apply at the final
57
+ * GitHub-comment boundary.
58
+ */
59
+ export const formatWorkingSessionSummaryMarkdown = text => {
60
+ if (typeof text !== 'string' || !text) return text;
61
+
62
+ const lines = text.split('\n');
63
+ const output = [];
64
+ let inFence = false;
65
+ let previousNonEmpty = '';
66
+
67
+ for (let index = 0; index < lines.length; ) {
68
+ const line = lines[index];
69
+ if (/^\s*(```|~~~)/.test(line)) {
70
+ inFence = !inFence;
71
+ output.push(line);
72
+ if (line.trim()) previousNonEmpty = line;
73
+ index += 1;
74
+ continue;
75
+ }
76
+
77
+ if (!inFence && line.trim() && previousNonEmpty.trimEnd().endsWith(':')) {
78
+ let end = index;
79
+ while (end < lines.length && lines[end].trim()) end += 1;
80
+ const block = lines.slice(index, end);
81
+ const looksStructured = block.length >= 2 && block.some(blockLine => /^(?:\t| {2,})/.test(blockLine));
82
+ if (looksStructured) {
83
+ output.push('```text', ...block, '```');
84
+ previousNonEmpty = '```';
85
+ index = end;
86
+ continue;
87
+ }
88
+ }
89
+
90
+ output.push(line);
91
+ if (line.trim()) previousNonEmpty = line;
92
+ index += 1;
93
+ }
94
+
95
+ return output.join('\n');
96
+ };
97
+
52
98
  /**
53
99
  * The line appended when the pull request still has an empty diff.
54
100
  *
@@ -62,4 +108,4 @@ export const buildNoChangesNotice = changeStats => {
62
108
  return '> ⚠️ This pull request still contains no changes - nothing was implemented yet.';
63
109
  };
64
110
 
65
- export default { buildNoChangesNotice, redactWorkspacePaths, WORKSPACE_PATH_PLACEHOLDER };
111
+ export default { buildNoChangesNotice, formatWorkingSessionSummaryMarkdown, redactWorkspacePaths, WORKSPACE_PATH_PLACEHOLDER };