@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.
- package/CHANGELOG.md +18 -0
- package/package.json +4 -1
- package/src/agent-command.lib.mjs +74 -0
- package/src/agent.lib.mjs +59 -34
- package/src/agentic-cli-updater.lib.mjs +241 -0
- package/src/claude.connection.lib.mjs +209 -0
- package/src/claude.lib.mjs +6 -202
- package/src/codex.lib.mjs +0 -128
- package/src/formal-ai-isolation.lib.mjs +62 -0
- package/src/formal-ai-maintenance.lib.mjs +106 -0
- package/src/formal-ai-model.lib.mjs +25 -0
- package/src/formal-ai-runtime.lib.mjs +10 -0
- package/src/formal-ai-sidecar.lib.mjs +565 -0
- package/src/formal-ai-updater.lib.mjs +294 -0
- package/src/formal-ai-version.lib.mjs +100 -0
- package/src/formal-ai.lib.mjs +11 -16
- package/src/github-rate-limit.lib.mjs +3 -0
- package/src/github-url-parser.lib.mjs +255 -0
- package/src/github.lib.mjs +22 -343
- package/src/hive.mjs +0 -152
- package/src/interactive-mode.lib.mjs +0 -43
- package/src/isolation-runner.lib.mjs +44 -173
- package/src/limits.lib.mjs +0 -89
- package/src/model-args.lib.mjs +32 -0
- package/src/models/index.mjs +5 -19
- package/src/session-monitor.lib.mjs +14 -172
- package/src/solve.auto-merge.lib.mjs +70 -164
- package/src/solve.mjs +31 -193
- package/src/solve.repository.lib.mjs +0 -83
- package/src/solve.results.lib.mjs +2 -92
- package/src/solve.session.lib.mjs +52 -19
- package/src/solve.tool-uncommitted.lib.mjs +22 -0
- package/src/state-lock.lib.mjs +82 -0
- package/src/telegram-bot.mjs +17 -65
- package/src/telegram-fix-command.lib.mjs +1 -8
- package/src/telegram-merge-queue.lib.mjs +3 -155
- package/src/telegram-solve-queue.lib.mjs +9 -168
- package/src/telegram-task-command.lib.mjs +1 -8
- package/src/use-m-bootstrap.lib.mjs +6 -5
- package/src/use-with-retry.lib.mjs +128 -2
- 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
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
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
|
|
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://
|
|
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
|
-
|
|
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 };
|