@link-assistant/hive-mind 1.52.1 â 1.53.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 +16 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +18 -11
- package/src/agent.prompts.lib.mjs +18 -0
- package/src/claude.lib.mjs +16 -5
- package/src/claude.prompts.lib.mjs +1 -0
- package/src/codex.lib.mjs +5 -0
- package/src/codex.prompts.lib.mjs +1 -0
- package/src/github.lib.mjs +59 -43
- package/src/interactive-mode.lib.mjs +12 -2
- package/src/opencode.lib.mjs +15 -0
- package/src/opencode.prompts.lib.mjs +18 -0
- package/src/playwright-mcp.lib.mjs +298 -0
- package/src/solve.auto-merge-helpers.lib.mjs +9 -6
- package/src/solve.auto-merge.lib.mjs +28 -20
- package/src/solve.config.lib.mjs +6 -1
- package/src/solve.mjs +36 -35
- package/src/solve.progress-monitoring.lib.mjs +16 -11
- package/src/solve.repo-setup.lib.mjs +8 -4
- package/src/solve.repository.lib.mjs +6 -4
- package/src/solve.restart-shared.lib.mjs +29 -2
- package/src/solve.results.lib.mjs +64 -3
- package/src/solve.session.lib.mjs +25 -13
- package/src/solve.watch.lib.mjs +7 -2
- package/src/tool-comments.lib.mjs +271 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Playwright MCP session-level disable/restore utilities.
|
|
3
|
+
if (typeof globalThis.use === 'undefined') {
|
|
4
|
+
globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
5
|
+
}
|
|
6
|
+
const { $ } = await use('command-stream');
|
|
7
|
+
const fs = (await use('fs')).promises;
|
|
8
|
+
const os = await use('os');
|
|
9
|
+
const path = (await use('path')).default;
|
|
10
|
+
|
|
11
|
+
export const getCommandResultCode = result => result?.code ?? result?.exitCode ?? null;
|
|
12
|
+
|
|
13
|
+
export const getCommandResultOutput = result => `${result?.stdout?.toString() || ''}${result?.stderr?.toString() || ''}`;
|
|
14
|
+
|
|
15
|
+
export const isCommandResultSuccess = result => getCommandResultCode(result) === 0;
|
|
16
|
+
|
|
17
|
+
export const checkPlaywrightMcpPackageAvailability = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const result = await $`timeout 5 npx --no-install @playwright/mcp --help 2>&1`.catch(() => null);
|
|
20
|
+
if (isCommandResultSuccess(result)) return true;
|
|
21
|
+
const npmResult = await $`timeout 5 npm ls -g @playwright/mcp 2>&1`.catch(() => null);
|
|
22
|
+
return getCommandResultOutput(npmResult).includes('@playwright/mcp');
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const parseCodexMcpServerNames = output =>
|
|
29
|
+
output
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.map(line => line.trim())
|
|
32
|
+
.filter(line => line && !line.startsWith('Name '))
|
|
33
|
+
.map(line => line.split(/\s+/)[0])
|
|
34
|
+
.filter(name => /^[A-Za-z0-9_-]+$/.test(name));
|
|
35
|
+
|
|
36
|
+
export const getCodexPlaywrightMcpDisableConfigArgs = async log => {
|
|
37
|
+
try {
|
|
38
|
+
const result = await $`timeout 5 codex mcp list 2>&1`.catch(() => null);
|
|
39
|
+
if (!isCommandResultSuccess(result)) return [];
|
|
40
|
+
const names = parseCodexMcpServerNames(getCommandResultOutput(result)).filter(name => name.toLowerCase().includes('playwright'));
|
|
41
|
+
if (names.length === 0) {
|
|
42
|
+
if (log) await log('đ No Codex Playwright MCP server registration found to disable for this session', { verbose: true });
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
if (log) {
|
|
46
|
+
await log(`đ Playwright MCP disabled for this Codex session via config override: ${names.join(', ')}`, {
|
|
47
|
+
verbose: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return names.flatMap(name => ['-c', `mcp_servers.${name}.enabled=false`]);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (log) await log(`â ī¸ Could not build Codex Playwright MCP disable override: ${err.message}`, { verbose: true });
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isPlainObject = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
58
|
+
|
|
59
|
+
const mergeDeep = (base, override) => {
|
|
60
|
+
const result = { ...(isPlainObject(base) ? base : {}) };
|
|
61
|
+
for (const [key, value] of Object.entries(isPlainObject(override) ? override : {})) {
|
|
62
|
+
result[key] = isPlainObject(result[key]) && isPlainObject(value) ? mergeDeep(result[key], value) : value;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const stripJsonComments = input => {
|
|
68
|
+
let output = '';
|
|
69
|
+
let inString = false;
|
|
70
|
+
let quote = '';
|
|
71
|
+
let escaped = false;
|
|
72
|
+
for (let index = 0; index < input.length; index++) {
|
|
73
|
+
const char = input[index];
|
|
74
|
+
const next = input[index + 1];
|
|
75
|
+
if (inString) {
|
|
76
|
+
output += char;
|
|
77
|
+
if (escaped) {
|
|
78
|
+
escaped = false;
|
|
79
|
+
} else if (char === '\\') {
|
|
80
|
+
escaped = true;
|
|
81
|
+
} else if (char === quote) {
|
|
82
|
+
inString = false;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (char === '"' || char === "'") {
|
|
87
|
+
inString = true;
|
|
88
|
+
quote = char;
|
|
89
|
+
output += char;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '/' && next === '/') {
|
|
93
|
+
while (index < input.length && input[index] !== '\n') index++;
|
|
94
|
+
output += '\n';
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '/' && next === '*') {
|
|
98
|
+
index += 2;
|
|
99
|
+
while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) index++;
|
|
100
|
+
index++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
output += char;
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const stripTrailingCommas = input => {
|
|
109
|
+
let output = '';
|
|
110
|
+
let inString = false;
|
|
111
|
+
let quote = '';
|
|
112
|
+
let escaped = false;
|
|
113
|
+
for (let index = 0; index < input.length; index++) {
|
|
114
|
+
const char = input[index];
|
|
115
|
+
if (inString) {
|
|
116
|
+
output += char;
|
|
117
|
+
if (escaped) {
|
|
118
|
+
escaped = false;
|
|
119
|
+
} else if (char === '\\') {
|
|
120
|
+
escaped = true;
|
|
121
|
+
} else if (char === quote) {
|
|
122
|
+
inString = false;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === '"' || char === "'") {
|
|
127
|
+
inString = true;
|
|
128
|
+
quote = char;
|
|
129
|
+
output += char;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === ',') {
|
|
133
|
+
let lookahead = index + 1;
|
|
134
|
+
while (/\s/.test(input[lookahead] || '')) lookahead++;
|
|
135
|
+
if (input[lookahead] === '}' || input[lookahead] === ']') continue;
|
|
136
|
+
}
|
|
137
|
+
output += char;
|
|
138
|
+
}
|
|
139
|
+
return output;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const parseConfigContent = content => {
|
|
143
|
+
if (!content || typeof content !== 'string') return {};
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(content);
|
|
146
|
+
} catch {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(stripTrailingCommas(stripJsonComments(content)));
|
|
149
|
+
} catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const readConfigFile = async filePath => {
|
|
156
|
+
const content = await fs.readFile(filePath, 'utf-8').catch(() => null);
|
|
157
|
+
return parseConfigContent(content);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const pathExists = async filePath =>
|
|
161
|
+
fs
|
|
162
|
+
.stat(filePath)
|
|
163
|
+
.then(() => true)
|
|
164
|
+
.catch(() => false);
|
|
165
|
+
|
|
166
|
+
const findUpConfigPaths = async (startDir, filenames) => {
|
|
167
|
+
const results = [];
|
|
168
|
+
let dir = startDir || process.cwd();
|
|
169
|
+
while (dir) {
|
|
170
|
+
for (const file of filenames) results.push(path.join(dir, file));
|
|
171
|
+
if (await pathExists(path.join(dir, '.git'))) break;
|
|
172
|
+
const parent = path.dirname(dir);
|
|
173
|
+
if (parent === dir) break;
|
|
174
|
+
dir = parent;
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const configFilesInDir = dir => (dir ? ['config.json', 'opencode.json', 'opencode.jsonc'].map(file => path.join(dir, file)) : []);
|
|
180
|
+
|
|
181
|
+
const isPlaywrightMcpEntry = (name, config) => {
|
|
182
|
+
const haystack = `${name || ''} ${JSON.stringify(config || {})}`.toLowerCase();
|
|
183
|
+
return haystack.includes('playwright');
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const collectPlaywrightMcpServerNames = (...configs) => {
|
|
187
|
+
const names = new Set();
|
|
188
|
+
for (const config of configs.flat()) {
|
|
189
|
+
if (!isPlainObject(config?.mcp)) continue;
|
|
190
|
+
for (const [name, mcpConfig] of Object.entries(config.mcp)) {
|
|
191
|
+
if (isPlaywrightMcpEntry(name, mcpConfig)) names.add(name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...names];
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const buildPlaywrightMcpDisableConfig = (serverNames = []) => {
|
|
198
|
+
const names = [...new Set(['playwright', ...serverNames].filter(Boolean))];
|
|
199
|
+
const mcp = {};
|
|
200
|
+
const tools = {
|
|
201
|
+
'*playwright*': false,
|
|
202
|
+
'mcp__playwright__*': false,
|
|
203
|
+
};
|
|
204
|
+
for (const name of names) {
|
|
205
|
+
mcp[name] = {
|
|
206
|
+
type: 'local',
|
|
207
|
+
command: ['npx', '-y', '@playwright/mcp@latest'],
|
|
208
|
+
enabled: false,
|
|
209
|
+
};
|
|
210
|
+
tools[`${name}_*`] = false;
|
|
211
|
+
tools[`mcp__${name}__*`] = false;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
$schema: 'https://opencode.ai/config.json',
|
|
215
|
+
mcp,
|
|
216
|
+
tools,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const mergePlaywrightMcpDisableConfigContent = (existingContent = '', serverNames = []) => {
|
|
221
|
+
const existingConfig = parseConfigContent(existingContent);
|
|
222
|
+
const detectedNames = collectPlaywrightMcpServerNames(existingConfig);
|
|
223
|
+
const disableConfig = buildPlaywrightMcpDisableConfig([...serverNames, ...detectedNames]);
|
|
224
|
+
return JSON.stringify(mergeDeep(existingConfig, disableConfig), null, 2);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const collectPlaywrightMcpServerNamesFromFiles = async filePaths => {
|
|
228
|
+
const configs = [];
|
|
229
|
+
for (const filePath of filePaths) configs.push(await readConfigFile(filePath));
|
|
230
|
+
return collectPlaywrightMcpServerNames(configs);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const getConfigHome = env => env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
234
|
+
|
|
235
|
+
const getOpenCodeConfigFilePaths = async ({ env = process.env, cwd = process.cwd() } = {}) => [...configFilesInDir(path.join(getConfigHome(env), 'opencode')), ...(env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : []), ...(await findUpConfigPaths(cwd, ['opencode.jsonc', 'opencode.json'])), ...configFilesInDir(env.OPENCODE_CONFIG_DIR)];
|
|
236
|
+
|
|
237
|
+
const getAgentConfigFilePaths = async ({ env = process.env, cwd = process.cwd() } = {}) => [...configFilesInDir(path.join(getConfigHome(env), 'link-assistant-agent')), ...(env.LINK_ASSISTANT_AGENT_CONFIG ? [env.LINK_ASSISTANT_AGENT_CONFIG] : []), ...(env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : []), ...(await findUpConfigPaths(cwd, ['opencode.jsonc', 'opencode.json'])), ...configFilesInDir(path.join(cwd, '.link-assistant-agent')), ...configFilesInDir(path.join(cwd, '.opencode')), ...configFilesInDir(env.LINK_ASSISTANT_AGENT_CONFIG_DIR), ...configFilesInDir(env.OPENCODE_CONFIG_DIR)];
|
|
238
|
+
|
|
239
|
+
export const getOpenCodePlaywrightMcpDisableEnv = async ({ env = process.env, cwd = process.cwd(), includeConfigFiles = true, log } = {}) => {
|
|
240
|
+
const inlineConfig = env.OPENCODE_CONFIG_CONTENT || '';
|
|
241
|
+
const names = collectPlaywrightMcpServerNames(parseConfigContent(inlineConfig));
|
|
242
|
+
if (includeConfigFiles) {
|
|
243
|
+
names.push(...(await collectPlaywrightMcpServerNamesFromFiles(await getOpenCodeConfigFilePaths({ env, cwd }))));
|
|
244
|
+
}
|
|
245
|
+
const uniqueNames = [...new Set(names)];
|
|
246
|
+
const displayNames = [...new Set(['playwright', ...uniqueNames])];
|
|
247
|
+
if (log) await log(`đ OpenCode Playwright MCP disabled through OPENCODE_CONFIG_CONTENT for: ${displayNames.join(', ')}`, { verbose: true });
|
|
248
|
+
return {
|
|
249
|
+
OPENCODE_CONFIG_CONTENT: mergePlaywrightMcpDisableConfigContent(inlineConfig, uniqueNames),
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const getAgentPlaywrightMcpDisableEnv = async ({ env = process.env, cwd = process.cwd(), includeConfigFiles = true, log } = {}) => {
|
|
254
|
+
const agentInlineConfig = env.LINK_ASSISTANT_AGENT_CONFIG_CONTENT || env.OPENCODE_CONFIG_CONTENT || '';
|
|
255
|
+
const names = collectPlaywrightMcpServerNames(parseConfigContent(agentInlineConfig), parseConfigContent(env.OPENCODE_CONFIG_CONTENT || ''));
|
|
256
|
+
if (includeConfigFiles) {
|
|
257
|
+
names.push(...(await collectPlaywrightMcpServerNamesFromFiles(await getAgentConfigFilePaths({ env, cwd }))));
|
|
258
|
+
}
|
|
259
|
+
const uniqueNames = [...new Set(names)];
|
|
260
|
+
const displayNames = [...new Set(['playwright', ...uniqueNames])];
|
|
261
|
+
const configContent = mergePlaywrightMcpDisableConfigContent(agentInlineConfig, uniqueNames);
|
|
262
|
+
if (log) await log(`đ Agent Playwright MCP disabled through LINK_ASSISTANT_AGENT_CONFIG_CONTENT for: ${displayNames.join(', ')}`, { verbose: true });
|
|
263
|
+
return {
|
|
264
|
+
LINK_ASSISTANT_AGENT_CONFIG_CONTENT: configContent,
|
|
265
|
+
OPENCODE_CONFIG_CONTENT: mergePlaywrightMcpDisableConfigContent(env.OPENCODE_CONFIG_CONTENT || agentInlineConfig, uniqueNames),
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/** Build a temporary MCP config JSON excluding Playwright, for use with --strict-mcp-config */
|
|
270
|
+
export const buildMcpConfigWithoutPlaywright = async log => {
|
|
271
|
+
try {
|
|
272
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
273
|
+
const claudeJson = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
|
|
274
|
+
const mcpServers = claudeJson.mcpServers || {};
|
|
275
|
+
const filtered = {};
|
|
276
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
277
|
+
if (name.toLowerCase().includes('playwright')) continue;
|
|
278
|
+
filtered[name] = config;
|
|
279
|
+
}
|
|
280
|
+
const tempConfigPath = path.join(os.tmpdir(), `claude-mcp-no-playwright-${Date.now()}-${process.pid}.json`);
|
|
281
|
+
await fs.writeFile(tempConfigPath, JSON.stringify({ mcpServers: filtered }, null, 2));
|
|
282
|
+
if (log) await log(`đ Created filtered MCP config (without Playwright): ${tempConfigPath}`, { verbose: true });
|
|
283
|
+
return tempConfigPath;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (log) await log(`â ī¸ Could not build filtered MCP config: ${err.message}`, { verbose: true });
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** Cascade --no-playwright-mcp to disable related flags */
|
|
291
|
+
export const cascadePlaywrightMcpDisable = async (argv, log) => {
|
|
292
|
+
if (argv.playwrightMcp === false) {
|
|
293
|
+
if (log) await log('đ Playwright MCP physically disabled via --no-playwright-mcp', { verbose: true });
|
|
294
|
+
argv.promptPlaywrightMcp = false;
|
|
295
|
+
argv.playwrightMcpAutoCleanup = false;
|
|
296
|
+
if (log) await log('âšī¸ --prompt-playwright-mcp and --playwright-mcp-auto-cleanup also disabled', { verbose: true });
|
|
297
|
+
}
|
|
298
|
+
};
|
|
@@ -35,6 +35,12 @@ const { reportError } = sentryLib;
|
|
|
35
35
|
const githubMergeLib = await import('./github-merge.lib.mjs');
|
|
36
36
|
const { checkPRMergeable, checkForBillingLimitError, getDetailedCIStatus, getWorkflowRunsForSha, getActiveRepoWorkflows, getCommitDate, checkWorkflowsHavePRTriggers, checkPreviousPRCommitsHadCI } = githubMergeLib;
|
|
37
37
|
|
|
38
|
+
// Issue #1625: Import centralized session-ending markers so the duplicate-
|
|
39
|
+
// search scope for checkForExistingComment() stays in lock-step with the
|
|
40
|
+
// markers actually embedded in tool-posted comments.
|
|
41
|
+
const toolComments = await import('./tool-comments.lib.mjs');
|
|
42
|
+
const { SESSION_ENDING_MARKERS } = toolComments;
|
|
43
|
+
|
|
38
44
|
/**
|
|
39
45
|
* Issue #1323: Check if a comment with specific content already exists on the PR
|
|
40
46
|
* This prevents duplicate status comments when multiple processes or restarts occur
|
|
@@ -85,14 +91,11 @@ export const checkForExistingComment = async (owner, repo, prNumber, commentSign
|
|
|
85
91
|
// Session-ending markers indicate the end of a working session,
|
|
86
92
|
// so any "Ready to merge" before it belongs to a previous session.
|
|
87
93
|
//
|
|
88
|
-
// Session-ending markers
|
|
89
|
-
// -
|
|
90
|
-
// (Solution Draft Log, Auto-restart Log, Auto-restart-until-mergeable Log, etc.)
|
|
91
|
-
// - "AI Work Session Completed" â posted when logs are not attached
|
|
92
|
-
const sessionEndingMarkers = ['Now working session is ended', 'AI Work Session Completed'];
|
|
94
|
+
// Issue #1625: Session-ending markers are now imported from
|
|
95
|
+
// tool-comments.lib.mjs (single source of truth for all markers).
|
|
93
96
|
let searchStartIndex = 0;
|
|
94
97
|
for (let i = commentBodies.length - 1; i >= 0; i--) {
|
|
95
|
-
if (commentBodies[i] &&
|
|
98
|
+
if (commentBodies[i] && SESSION_ENDING_MARKERS.some(marker => commentBodies[i].includes(marker))) {
|
|
96
99
|
searchStartIndex = i + 1;
|
|
97
100
|
if (verbose) {
|
|
98
101
|
console.log(`[VERBOSE] Found last session-ending comment at index ${i}, searching from index ${searchStartIndex}`);
|
|
@@ -54,6 +54,10 @@ import { limitReset } from './config.lib.mjs';
|
|
|
54
54
|
const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
|
|
55
55
|
const { checkForExistingComment, checkForNonBotComments, getMergeBlockers } = autoMergeHelpers;
|
|
56
56
|
|
|
57
|
+
// Issue #1625: Shared marker constants + posting/tracking helpers
|
|
58
|
+
const toolComments = await import('./tool-comments.lib.mjs');
|
|
59
|
+
const { READY_TO_MERGE_MARKER, AUTO_RESTART_MARKER, AUTO_MERGED_MARKER, postTrackedComment } = toolComments;
|
|
60
|
+
|
|
57
61
|
// Issue #1574: Interruptible sleep so CTRL+C is never blocked by a lingering timer
|
|
58
62
|
const { interruptibleSleep } = await import('./interruptible-sleep.lib.mjs');
|
|
59
63
|
|
|
@@ -263,8 +267,8 @@ export const watchUntilMergeable = async params => {
|
|
|
263
267
|
try {
|
|
264
268
|
// Issue #1345: Differentiate message when no CI is configured
|
|
265
269
|
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
|
|
266
|
-
const commentBody = `## đ
|
|
267
|
-
await
|
|
270
|
+
const commentBody = `## đ ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind.\n${ciLine}\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
|
|
271
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
268
272
|
} catch {
|
|
269
273
|
// Don't fail if comment posting fails
|
|
270
274
|
}
|
|
@@ -291,19 +295,20 @@ export const watchUntilMergeable = async params => {
|
|
|
291
295
|
// Issue #1567: Cross-process deduplication â check if another process already
|
|
292
296
|
// posted a "Ready to merge" comment. This catches the case where two concurrent
|
|
293
297
|
// watchUntilMergeable processes both detect mergeability simultaneously.
|
|
294
|
-
const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber,
|
|
298
|
+
const hasExistingReadyComment = await checkForExistingComment(owner, repo, prNumber, `## â
${READY_TO_MERGE_MARKER}`, argv.verbose);
|
|
295
299
|
if (hasExistingReadyComment) {
|
|
296
|
-
await log(formatAligned('',
|
|
300
|
+
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment (already posted by another process)`, '', 2));
|
|
297
301
|
readyToMergeCommentPosted = true;
|
|
298
302
|
} else {
|
|
299
303
|
// Issue #1345: Differentiate message when no CI is configured
|
|
300
304
|
const ciLine = noCiConfigured ? '- No CI/CD checks are configured for this repository' : noCiTriggered ? (workflowRunConclusions ? `- CI workflows completed without executing (${workflowRunConclusions})` : '- CI workflows exist but were not triggered for this commit') : '- All CI checks have passed';
|
|
301
|
-
const commentBody = `## â
|
|
302
|
-
|
|
305
|
+
const commentBody = `## â
${READY_TO_MERGE_MARKER}\n\nThis pull request is now ready to be merged:\n${ciLine}\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*`;
|
|
306
|
+
// Issue #1625: Track this comment ID so it can't falsely count as an AI-authored comment
|
|
307
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
303
308
|
readyToMergeCommentPosted = true;
|
|
304
309
|
}
|
|
305
310
|
} else {
|
|
306
|
-
await log(formatAligned('',
|
|
311
|
+
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment (already posted this session)`, '', 2));
|
|
307
312
|
}
|
|
308
313
|
} catch {
|
|
309
314
|
// Don't fail if comment posting fails
|
|
@@ -370,7 +375,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
370
375
|
|
|
371
376
|
---
|
|
372
377
|
*Detected by hive-mind with --auto-restart-until-mergeable flag. This is NOT a code issue - human intervention is required.*`;
|
|
373
|
-
await
|
|
378
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
374
379
|
await log(formatAligned('', 'đŦ Posted billing limit notification to PR', '', 2));
|
|
375
380
|
} catch (commentError) {
|
|
376
381
|
reportError(commentError, {
|
|
@@ -488,8 +493,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
488
493
|
// Post a comment to PR about the restart
|
|
489
494
|
// Issue #1356: Include restart count for tracking and add deduplication
|
|
490
495
|
try {
|
|
491
|
-
const commentBody = `## đ
|
|
492
|
-
|
|
496
|
+
const commentBody = `## đ ${AUTO_RESTART_MARKER} triggered (iteration ${restartCount})\n\n**Reason:** ${restartReason}\n\nStarting new session to address the issues.\n\n---\n*Auto-restart-until-mergeable mode is active. Will continue until PR becomes mergeable.*`;
|
|
497
|
+
// Issue #1625: Track so this doesn't falsely count as an AI-authored comment
|
|
498
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
493
499
|
await log(formatAligned('', 'đŦ Posted auto-restart notification to PR', '', 2));
|
|
494
500
|
} catch (commentError) {
|
|
495
501
|
reportError(commentError, {
|
|
@@ -910,8 +916,8 @@ export const attemptAutoMerge = async params => {
|
|
|
910
916
|
|
|
911
917
|
// Post success comment
|
|
912
918
|
try {
|
|
913
|
-
const commentBody = `## đ
|
|
914
|
-
await
|
|
919
|
+
const commentBody = `## đ ${AUTO_MERGED_MARKER}\n\nThis pull request has been automatically merged by hive-mind after all CI checks passed and the PR became mergeable.\n\n---\n*Auto-merged by hive-mind with --auto-merge flag*`;
|
|
920
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
915
921
|
} catch {
|
|
916
922
|
// Don't fail if comment posting fails
|
|
917
923
|
}
|
|
@@ -954,14 +960,15 @@ export const startAutoRestartUntilMergeable = async params => {
|
|
|
954
960
|
|
|
955
961
|
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
956
962
|
try {
|
|
957
|
-
const readyToMergeSignature =
|
|
963
|
+
const readyToMergeSignature = `## â
${READY_TO_MERGE_MARKER}`;
|
|
958
964
|
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
959
965
|
if (!hasExistingComment) {
|
|
960
|
-
const commentBody = `## â
|
|
961
|
-
|
|
966
|
+
const commentBody = `## â
${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because this PR was created from a fork (no write access to the target repository).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag (fork mode)*`;
|
|
967
|
+
// Issue #1625: Track so this doesn't falsely count as AI-authored.
|
|
968
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
962
969
|
await log(formatAligned('', 'đŦ Posted merge readiness notification to PR', '', 2));
|
|
963
970
|
} else {
|
|
964
|
-
await log(formatAligned('',
|
|
971
|
+
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
|
|
965
972
|
}
|
|
966
973
|
} catch {
|
|
967
974
|
// Don't fail if comment posting fails
|
|
@@ -983,14 +990,15 @@ export const startAutoRestartUntilMergeable = async params => {
|
|
|
983
990
|
|
|
984
991
|
// Issue #1323: Post a comment to the PR notifying the maintainer (with deduplication)
|
|
985
992
|
try {
|
|
986
|
-
const readyToMergeSignature =
|
|
993
|
+
const readyToMergeSignature = `## â
${READY_TO_MERGE_MARKER}`;
|
|
987
994
|
const hasExistingComment = await checkForExistingComment(owner, repo, prNumber, readyToMergeSignature, argv.verbose);
|
|
988
995
|
if (!hasExistingComment) {
|
|
989
|
-
const commentBody = `## â
|
|
990
|
-
|
|
996
|
+
const commentBody = `## â
${READY_TO_MERGE_MARKER}\n\nThis pull request is ready to be merged. Auto-merge was requested (\`--auto-merge\`) but cannot be performed because the authenticated user lacks write access to \`${owner}/${repo}\` (current permission: \`${permission || 'unknown'}\`).\n\nPlease merge manually.\n\n---\n*hive-mind with --auto-merge flag*`;
|
|
997
|
+
// Issue #1625: Track so this doesn't falsely count as AI-authored.
|
|
998
|
+
await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: commentBody });
|
|
991
999
|
await log(formatAligned('', 'đŦ Posted merge readiness notification to PR', '', 2));
|
|
992
1000
|
} else {
|
|
993
|
-
await log(formatAligned('',
|
|
1001
|
+
await log(formatAligned('', `Skipping duplicate "${READY_TO_MERGE_MARKER}" comment`, '', 2));
|
|
994
1002
|
}
|
|
995
1003
|
} catch {
|
|
996
1004
|
// Don't fail if comment posting fails
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -366,7 +366,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
366
366
|
},
|
|
367
367
|
'prompt-playwright-mcp': {
|
|
368
368
|
type: 'boolean',
|
|
369
|
-
description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Supported for --tool claude and --tool
|
|
369
|
+
description: 'Enable Playwright MCP browser automation hints in system prompt (enabled by default, only takes effect if Playwright MCP is installed). Use --no-prompt-playwright-mcp to disable. Supported for --tool claude, --tool codex, --tool opencode, and --tool agent.',
|
|
370
370
|
default: true,
|
|
371
371
|
},
|
|
372
372
|
'prompt-check-sibling-pull-requests': {
|
|
@@ -384,6 +384,11 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
384
384
|
description: 'Path to examples folder used in system prompt. Set to empty string to disable examples folder prompt. Default: ./examples',
|
|
385
385
|
default: './examples',
|
|
386
386
|
},
|
|
387
|
+
'playwright-mcp': {
|
|
388
|
+
type: 'boolean',
|
|
389
|
+
description: 'Enable Playwright MCP server connection for this session (enabled by default). Use --no-playwright-mcp to physically disable the Playwright MCP server without affecting the global MCP registration. When disabled, also disables --prompt-playwright-mcp and --playwright-mcp-auto-cleanup. Supported for --tool claude, --tool codex, --tool opencode, and --tool agent.',
|
|
390
|
+
default: true,
|
|
391
|
+
},
|
|
387
392
|
'playwright-mcp-auto-cleanup': {
|
|
388
393
|
type: 'boolean',
|
|
389
394
|
description: 'Automatically remove .playwright-mcp/ folder before checking for uncommitted changes. This prevents browser automation artifacts from triggering auto-restart. Use --no-playwright-mcp-auto-cleanup to keep the folder for debugging.',
|
package/src/solve.mjs
CHANGED
|
@@ -54,6 +54,10 @@ const { handleAutoPrCreation } = await import('./solve.auto-pr.lib.mjs');
|
|
|
54
54
|
const { setupRepositoryAndClone, verifyDefaultBranchAndStatus } = await import('./solve.repo-setup.lib.mjs');
|
|
55
55
|
const { createOrCheckoutBranch } = await import('./solve.branch.lib.mjs');
|
|
56
56
|
const { startWorkSession, endWorkSession, SESSION_TYPES } = await import('./solve.session.lib.mjs');
|
|
57
|
+
// Issue #1625: centralized markers + tracked comment posting for solve.mjs's
|
|
58
|
+
// own usage-limit notifications (so they're excluded from the
|
|
59
|
+
// "did the AI post anything?" check in --auto-attach-solution-summary).
|
|
60
|
+
const { postTrackedComment, USAGE_LIMIT_REACHED_MARKER } = await import('./tool-comments.lib.mjs');
|
|
57
61
|
const { prepareFeedbackAndTimestamps, checkUncommittedChanges, checkForkActions } = await import('./solve.preparation.lib.mjs');
|
|
58
62
|
const { validateAndExitOnInvalidModel } = await import('./models/index.mjs');
|
|
59
63
|
const { autoAcceptInviteForRepo } = await import('./solve.accept-invite.lib.mjs');
|
|
@@ -707,12 +711,31 @@ try {
|
|
|
707
711
|
$,
|
|
708
712
|
});
|
|
709
713
|
|
|
714
|
+
const { cascadePlaywrightMcpDisable } = await import('./playwright-mcp.lib.mjs');
|
|
715
|
+
await cascadePlaywrightMcpDisable(argv, log);
|
|
716
|
+
|
|
717
|
+
async function resolvePlaywrightMcp(checkFn) {
|
|
718
|
+
if (argv.playwrightMcp === false) return;
|
|
719
|
+
if (argv.promptPlaywrightMcp) {
|
|
720
|
+
const available = await checkFn();
|
|
721
|
+
if (available) {
|
|
722
|
+
await log('đ Playwright MCP detected - enabling browser automation hints', { verbose: true });
|
|
723
|
+
} else {
|
|
724
|
+
await log('âšī¸ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
|
|
725
|
+
argv.promptPlaywrightMcp = false;
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
await log('âšī¸ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
710
732
|
// Execute tool command with all prompts and settings
|
|
711
733
|
let toolResult;
|
|
712
734
|
if (argv.tool === 'opencode') {
|
|
713
735
|
const opencodeLib = await import('./opencode.lib.mjs');
|
|
714
|
-
const { executeOpenCode } = opencodeLib;
|
|
736
|
+
const { executeOpenCode, checkPlaywrightMcpAvailability: checkOpenCodePlaywrightMcp } = opencodeLib;
|
|
715
737
|
const opencodePath = process.env.OPENCODE_PATH || 'opencode';
|
|
738
|
+
await resolvePlaywrightMcp(checkOpenCodePlaywrightMcp);
|
|
716
739
|
|
|
717
740
|
toolResult = await executeOpenCode({
|
|
718
741
|
issueUrl,
|
|
@@ -742,18 +765,7 @@ try {
|
|
|
742
765
|
const codexLib = await import('./codex.lib.mjs');
|
|
743
766
|
const { executeCodex, checkPlaywrightMcpAvailability } = codexLib;
|
|
744
767
|
const codexPath = process.env.CODEX_PATH || 'codex';
|
|
745
|
-
|
|
746
|
-
if (argv.promptPlaywrightMcp) {
|
|
747
|
-
const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
|
|
748
|
-
if (playwrightMcpAvailable) {
|
|
749
|
-
await log('đ Playwright MCP detected - enabling browser automation hints', { verbose: true });
|
|
750
|
-
} else {
|
|
751
|
-
await log('âšī¸ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
|
|
752
|
-
argv.promptPlaywrightMcp = false;
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
await log('âšī¸ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
|
|
756
|
-
}
|
|
768
|
+
await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
|
|
757
769
|
|
|
758
770
|
toolResult = await executeCodex({
|
|
759
771
|
issueUrl,
|
|
@@ -781,8 +793,9 @@ try {
|
|
|
781
793
|
});
|
|
782
794
|
} else if (argv.tool === 'agent') {
|
|
783
795
|
const agentLib = await import('./agent.lib.mjs');
|
|
784
|
-
const { executeAgent } = agentLib;
|
|
796
|
+
const { executeAgent, checkPlaywrightMcpAvailability: checkAgentPlaywrightMcp } = agentLib;
|
|
785
797
|
const agentPath = process.env.AGENT_PATH || 'agent';
|
|
798
|
+
await resolvePlaywrightMcp(checkAgentPlaywrightMcp);
|
|
786
799
|
|
|
787
800
|
toolResult = await executeAgent({
|
|
788
801
|
issueUrl,
|
|
@@ -810,20 +823,8 @@ try {
|
|
|
810
823
|
});
|
|
811
824
|
} else {
|
|
812
825
|
// Default to Claude
|
|
813
|
-
// Check for Playwright MCP availability if using Claude tool
|
|
814
826
|
if (argv.tool === 'claude' || !argv.tool) {
|
|
815
|
-
|
|
816
|
-
if (argv.promptPlaywrightMcp) {
|
|
817
|
-
const playwrightMcpAvailable = await checkPlaywrightMcpAvailability();
|
|
818
|
-
if (playwrightMcpAvailable) {
|
|
819
|
-
await log('đ Playwright MCP detected - enabling browser automation hints', { verbose: true });
|
|
820
|
-
} else {
|
|
821
|
-
await log('âšī¸ Playwright MCP not detected - browser automation hints will be disabled', { verbose: true });
|
|
822
|
-
argv.promptPlaywrightMcp = false;
|
|
823
|
-
}
|
|
824
|
-
} else {
|
|
825
|
-
await log('âšī¸ Playwright MCP explicitly disabled via --no-prompt-playwright-mcp', { verbose: true });
|
|
826
|
-
}
|
|
827
|
+
await resolvePlaywrightMcp(checkPlaywrightMcpAvailability);
|
|
827
828
|
}
|
|
828
829
|
const claudeResult = await executeClaude({
|
|
829
830
|
issueUrl,
|
|
@@ -967,11 +968,11 @@ try {
|
|
|
967
968
|
// Format the reset time with relative time and UTC conversion if available
|
|
968
969
|
const timezone = global.limitTimezone || null;
|
|
969
970
|
const formattedResetTime = resetTime ? formatResetTimeWithRelative(resetTime, timezone) : null;
|
|
970
|
-
const failureComment = formattedResetTime ? `â
|
|
971
|
+
const failureComment = formattedResetTime ? `â **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. The limit will reset at: **${formattedResetTime}**\n\n${resumeSection}` : `â **${USAGE_LIMIT_REACHED_MARKER}**\n\nThe AI tool has reached its usage limit. Please wait for the limit to reset.\n\n${resumeSection}`;
|
|
971
972
|
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
await log(
|
|
973
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: failureComment });
|
|
974
|
+
if (posted.ok) {
|
|
975
|
+
await log(` Posted failure comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
975
976
|
}
|
|
976
977
|
} catch (error) {
|
|
977
978
|
await log(` Warning: Could not post failure comment: ${cleanErrorMessage(error)}`, { verbose: true });
|
|
@@ -1052,11 +1053,11 @@ try {
|
|
|
1052
1053
|
// Format reset time with relative time and UTC for better user understanding
|
|
1053
1054
|
// See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
1054
1055
|
const waitingResetTimeFormatted = formatResetTimeWithRelative(global.limitResetTime, global.limitTimezone || null) || global.limitResetTime;
|
|
1055
|
-
const waitingComment = `âŗ
|
|
1056
|
+
const waitingComment = `âŗ **${USAGE_LIMIT_REACHED_MARKER} - Waiting to ${limitContinueMode === 'restart' ? 'Restart' : 'Continue'}**\n\nThe AI tool has reached its usage limit. ${continueModeName} is enabled.\n\n**Reset time:** ${waitingResetTimeFormatted}\n**Wait time:** ${formatWaitTime(waitMs)} (days:hours:minutes:seconds)\n\n${continueDescription}\n\nSession ID: \`${sessionId}\``;
|
|
1056
1057
|
|
|
1057
|
-
const
|
|
1058
|
-
if (
|
|
1059
|
-
await log(
|
|
1058
|
+
const posted = await postTrackedComment({ $, owner, repo, targetNumber: prNumber, body: waitingComment });
|
|
1059
|
+
if (posted.ok) {
|
|
1060
|
+
await log(` Posted waiting comment to PR${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
1060
1061
|
}
|
|
1061
1062
|
} catch (error) {
|
|
1062
1063
|
await log(` Warning: Could not post waiting comment: ${cleanErrorMessage(error)}`, { verbose: true });
|