@link-assistant/hive-mind 1.50.11 → 1.50.13

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,19 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.50.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 3eb1428: Preserve GitHub issue-closing links after temporary auto-restart sessions edit pull request descriptions.
8
+
9
+ ## 1.50.12
10
+
11
+ ### Patch Changes
12
+
13
+ - 065deae: ## Summary
14
+
15
+ Fix Playwright MCP setup guidance and verification for Codex environments.
16
+
3
17
  ## 1.50.11
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -174,6 +174,10 @@ claude
174
174
  # Optionally test Claude connection
175
175
  claude -p hi --model haiku
176
176
 
177
+ # Verify Playwright MCP is registered for both CLIs in this container image
178
+ claude mcp list | grep playwright
179
+ codex mcp list | grep playwright
180
+
177
181
  # You might need to update hive-mind and agent to latest versions:
178
182
  bun install -g @link-assistant/hive-mind
179
183
  bun install -g @link-assistant/agent
@@ -212,6 +216,11 @@ docker run -dit --user sandbox --name hive-mind --restart unless-stopped \
212
216
  SANDBOX_UID=$(docker exec hive-mind id -u sandbox)
213
217
  chown -R $SANDBOX_UID:$SANDBOX_UID /root/.hive-mind/claude /root/.hive-mind/codex /root/.hive-mind/gh
214
218
  chown $SANDBOX_UID:$SANDBOX_UID /root/.hive-mind/claude.json
219
+
220
+ # Important: mounted ~/.codex data overrides the image-baked Codex config.
221
+ # If the host directory was created before Playwright MCP was added to the image,
222
+ # re-register it once inside the running container:
223
+ docker exec -it hive-mind bash -lc 'codex mcp list && codex mcp add playwright -- npx -y @playwright/mcp@latest --isolated --headless --no-sandbox --timeout-action=600000 --viewport-size 1920x1080'
215
224
  ```
216
225
 
217
226
  **Benefits of Docker:**
@@ -222,6 +231,8 @@ chown $SANDBOX_UID:$SANDBOX_UID /root/.hive-mind/claude.json
222
231
  - ✅ Easy to run multiple instances with different GitHub accounts
223
232
  - ✅ Consistent environment across different machines
224
233
 
234
+ The Docker image itself now registers Playwright MCP for both Claude and Codex during build, and CI verifies those registrations in the built container. If `codex mcp list` is still empty in a running container, the usual cause is not the published image itself but a mounted `/workspace/.codex` directory from the host that replaces the image's default Codex configuration.
235
+
225
236
  See [docs/DOCKER.md](./docs/DOCKER.md) for advanced Docker usage.
226
237
 
227
238
  #### Stoping and removing docker container
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.50.11",
3
+ "version": "1.50.13",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -13,15 +13,15 @@
13
13
  "hive-telegram-bot": "./src/telegram-bot.mjs"
14
14
  },
15
15
  "scripts": {
16
- "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
16
+ "test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
17
17
  "test:queue": "node tests/solve-queue.test.mjs",
18
18
  "test:limits-display": "node tests/limits-display.test.mjs",
19
19
  "test:usage-limit": "node tests/test-usage-limit.mjs",
20
20
  "lint": "eslint 'src/**/*.{js,mjs,cjs}'",
21
21
  "lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' --fix",
22
22
  "check:duplication": "jscpd .",
23
- "format": "prettier --write \"**/*.{js,mjs,json,md}\"",
24
- "format:check": "prettier --check \"**/*.{js,mjs,json,md}\"",
23
+ "format": "prettier --write \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
24
+ "format:check": "prettier --check \"**/*.{js,mjs,json,md}\" --ignore-path .prettierignore",
25
25
  "changeset": "changeset",
26
26
  "changeset:version": "changeset version",
27
27
  "changeset:publish": "npm run build:pre && changeset publish",
@@ -0,0 +1,34 @@
1
+ ## Summary
2
+
3
+ Fixes #1606 by documenting and verifying Playwright MCP registration for Codex in addition to Claude Code, including the Docker-specific case where persisted Codex state overrides the image defaults.
4
+
5
+ ## Root Cause
6
+
7
+ The reported environment had `@playwright/mcp` installed and registered in Claude, but `codex mcp list` had no configured servers. The immediate cause was missing Codex MCP registration. Local reproduction confirmed that `/workspace/.codex/config.toml` can exist without a Playwright MCP entry and that `codex mcp add playwright ...` fixes the state immediately. In Docker deployments, the most likely explanation is that a host-mounted `/workspace/.codex` directory preserved an older Codex config and replaced the image-baked MCP registration. Existing docs and helper scripts also focused mainly on Claude setup, so the mismatch was easy to miss even though `/version` already reported it correctly.
8
+
9
+ ## Changes
10
+
11
+ - added regression coverage for the mixed MCP state where Claude is connected and Codex is not
12
+ - updated Playwright MCP verification and integration scripts to check Codex MCP registration explicitly
13
+ - updated Docker verification to fail if Claude or Codex is missing the Playwright MCP registration
14
+ - updated configuration and Docker docs to include both `claude mcp add ...` and `codex mcp add ...`
15
+ - documented that mounting `/workspace/.codex` can override the image defaults and reintroduce the problem
16
+ - added the investigation record and collected evidence under `docs/case-studies/issue-1606`
17
+
18
+ ## Reproduction
19
+
20
+ 1. Install `@playwright/mcp` and register it only with Claude.
21
+ 2. Run `claude mcp list` and confirm `playwright` is present.
22
+ 3. Run `codex mcp list` and observe `No MCP servers configured yet`.
23
+ 4. Run `/version` and observe `Playwright MCP: <version> | Claude Code: connected | Codex: not connected`.
24
+
25
+ ## Verification
26
+
27
+ - `node tests/test-version-info.mjs`
28
+ - `node tests/test-version-parsing.mjs`
29
+ - `bash scripts/verify-docker-image.sh`
30
+
31
+ ## Evidence
32
+
33
+ - case study: `docs/case-studies/issue-1606/README.md`
34
+ - PR: https://github.com/link-assistant/hive-mind/pull/1607
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Helpers for preserving GitHub issue-closing links in pull request bodies.
5
+ */
6
+
7
+ import { hasGitHubLinkingKeyword } from './github-linking.lib.mjs';
8
+
9
+ /**
10
+ * Build the issue reference text to use in a GitHub closing keyword.
11
+ *
12
+ * @param {Object} options
13
+ * @param {string|number} options.issueNumber
14
+ * @param {string|null} [options.owner]
15
+ * @param {string|null} [options.repo]
16
+ * @param {boolean} [options.fork]
17
+ * @returns {string}
18
+ */
19
+ export function buildIssueReference({ issueNumber, owner = null, repo = null, fork = false } = {}) {
20
+ if (!issueNumber) {
21
+ return '';
22
+ }
23
+
24
+ if (fork && owner && repo) {
25
+ return `${owner}/${repo}#${issueNumber}`;
26
+ }
27
+
28
+ return `#${issueNumber}`;
29
+ }
30
+
31
+ /**
32
+ * Ensure a pull request body has a GitHub-recognized closing keyword for the issue.
33
+ *
34
+ * @param {string|null|undefined} prBody
35
+ * @param {Object} options
36
+ * @param {string|number} options.issueNumber
37
+ * @param {string|null} [options.owner]
38
+ * @param {string|null} [options.repo]
39
+ * @param {boolean} [options.fork]
40
+ * @returns {{body: string, updated: boolean, issueRef: string}}
41
+ */
42
+ export function ensureIssueLinkInPullRequestBody(prBody, { issueNumber, owner = null, repo = null, fork = false } = {}) {
43
+ const body = prBody ?? '';
44
+ const issueRef = buildIssueReference({ issueNumber, owner, repo, fork });
45
+
46
+ if (!issueNumber) {
47
+ return { body, updated: false, issueRef };
48
+ }
49
+
50
+ const hasLinkingKeyword = hasGitHubLinkingKeyword(body, issueNumber, owner, repo);
51
+ if (hasLinkingKeyword) {
52
+ return { body, updated: false, issueRef };
53
+ }
54
+
55
+ const separator = body.length > 0 ? '\n\n' : '';
56
+ return {
57
+ body: `${body}${separator}Fixes ${issueRef}`,
58
+ updated: true,
59
+ issueRef,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Parse GraphQL closingIssuesReferences stdout into issue number strings.
65
+ *
66
+ * @param {string|Buffer|null|undefined} output
67
+ * @returns {string[]}
68
+ */
69
+ export function parseClosingIssueNumbers(output) {
70
+ return String(output ?? '')
71
+ .trim()
72
+ .split('\n')
73
+ .map(value => value.trim())
74
+ .filter(Boolean);
75
+ }
76
+
77
+ /**
78
+ * Check whether parsed closing issue numbers contain the requested issue number.
79
+ *
80
+ * @param {Array<string|number>} linkedIssues
81
+ * @param {string|number} issueNumber
82
+ * @returns {boolean}
83
+ */
84
+ export function closingIssueNumbersContain(linkedIssues, issueNumber) {
85
+ if (!issueNumber || !Array.isArray(linkedIssues)) {
86
+ return false;
87
+ }
88
+
89
+ const expectedIssueNumber = String(issueNumber);
90
+ return linkedIssues.map(value => String(value).trim()).includes(expectedIssueNumber);
91
+ }
@@ -3,6 +3,8 @@
3
3
  * Handles automatic creation of draft pull requests with initial commits
4
4
  */
5
5
 
6
+ import { closingIssueNumbersContain, parseClosingIssueNumbers } from './pr-issue-linking.lib.mjs';
7
+
6
8
  export async function handleAutoPrCreation({ argv, tempDir, branchName, issueNumber, owner, repo, defaultBranch, forkedRepo, isContinueMode, prNumber, log, formatAligned, $, reportError, path, fs }) {
7
9
  // Skip auto-PR creation if:
8
10
  // 1. Auto-PR creation is disabled AND we're not in continue mode with no PR
@@ -1329,12 +1331,8 @@ ${prBody}`,
1329
1331
  const linkCheckResult = await $`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repo}") { pullRequest(number: ${localPrNumber}) { closingIssuesReferences(first: 10) { nodes { number } } } } }' --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`;
1330
1332
 
1331
1333
  if (linkCheckResult.code === 0) {
1332
- const linkedIssues = linkCheckResult.stdout
1333
- .toString()
1334
- .trim()
1335
- .split('\n')
1336
- .filter(n => n);
1337
- if (linkedIssues.includes(issueNumber)) {
1334
+ const linkedIssues = parseClosingIssueNumbers(linkCheckResult.stdout);
1335
+ if (closingIssueNumbersContain(linkedIssues, issueNumber)) {
1338
1336
  await log(formatAligned('✅', 'Link verified:', `Issue #${issueNumber} → PR #${localPrNumber}`));
1339
1337
  } else {
1340
1338
  // This is a problem - the link wasn't created
package/src/solve.mjs CHANGED
@@ -29,7 +29,7 @@ const { processAutoContinueForIssue } = autoContinue;
29
29
  const repository = await import('./solve.repository.lib.mjs');
30
30
  const { setupTempDirectory, cleanupTempDirectory } = repository;
31
31
  const results = await import('./solve.results.lib.mjs');
32
- const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildSolveResumeCommand, checkForAiCreatedComments, attachSolutionSummary } = results;
32
+ const { cleanupClaudeFile, showSessionSummary, verifyResults, buildClaudeResumeCommand, buildSolveResumeCommand, checkForAiCreatedComments, attachSolutionSummary, verifyPullRequestIssueLinkAfterAutoRestart } = results;
33
33
  const claudeLib = await import('./claude.lib.mjs');
34
34
  const { executeClaude, checkPlaywrightMcpAvailability } = claudeLib;
35
35
 
@@ -1358,6 +1358,8 @@ try {
1358
1358
  await log(` cd ${tempDir} && git push origin ${branchName}`, { level: 'error' });
1359
1359
  }
1360
1360
 
1361
+ await verifyPullRequestIssueLinkAfterAutoRestart({ prNumber, issueNumber, owner, repo, argv, cleanErrorMessage });
1362
+
1361
1363
  // Attach updated logs to PR after auto-restart completes
1362
1364
  // Issue #1154: Skip if logs were already uploaded by verifyResults() to prevent duplicates
1363
1365
  // Issue #1290: Always upload if auto-restart ran but last iteration's logs weren't uploaded
@@ -78,9 +78,9 @@ export const buildSolveResumeCommand = ({ issueUrl, sessionId, tool = null, mode
78
78
  const sentryLib = await import('./sentry.lib.mjs');
79
79
  const { reportError } = sentryLib;
80
80
 
81
- // Import GitHub linking detection library
82
- const githubLinking = await import('./github-linking.lib.mjs');
83
- const { hasGitHubLinkingKeyword } = githubLinking;
81
+ // Import pull request issue-link preservation helpers
82
+ const prIssueLinking = await import('./pr-issue-linking.lib.mjs');
83
+ const { buildIssueReference, ensureIssueLinkInPullRequestBody } = prIssueLinking;
84
84
 
85
85
  /**
86
86
  * Placeholder patterns used to detect auto-generated PR content that was not updated by the agent.
@@ -127,6 +127,83 @@ export const buildPRNotUpdatedHint = (titleNotUpdated, descriptionNotUpdated) =>
127
127
  return lines;
128
128
  };
129
129
 
130
+ /**
131
+ * Ensure an existing pull request body contains a GitHub closing keyword for the issue.
132
+ *
133
+ * @param {Object} options
134
+ * @param {string|number} options.prNumber - Pull request number
135
+ * @param {string|number} options.issueNumber - Issue number to link
136
+ * @param {string} options.owner - Repository owner
137
+ * @param {string} options.repo - Repository name
138
+ * @param {Object} [options.argv] - Parsed CLI arguments
139
+ * @param {Function} [options.command] - command-stream tagged template
140
+ * @param {Function} [options.logger] - Logger function
141
+ * @returns {Promise<{checked: boolean, updated: boolean, body: string, issueRef: string, error?: string}>}
142
+ */
143
+ export const ensurePullRequestIssueLink = async ({ prNumber, issueNumber, owner, repo, argv = {}, command = $, logger = log }) => {
144
+ if (!prNumber || !issueNumber || !owner || !repo) {
145
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error: 'missing required pull request or issue data' };
146
+ }
147
+
148
+ let prBody = '';
149
+ const prBodyResult = await command`gh pr view ${prNumber} --repo ${owner}/${repo} --json body --jq .body`;
150
+ if (prBodyResult.code !== 0) {
151
+ const error = prBodyResult.stderr ? prBodyResult.stderr.toString().trim() : 'Unknown error';
152
+ await logger(` ⚠️ Could not read PR body for issue link check: ${error}`);
153
+ return { checked: false, updated: false, body: prBody, issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error };
154
+ }
155
+
156
+ prBody = prBodyResult.stdout.toString();
157
+ const linkResult = ensureIssueLinkInPullRequestBody(prBody, {
158
+ issueNumber,
159
+ owner,
160
+ repo,
161
+ fork: argv.fork,
162
+ });
163
+
164
+ if (!linkResult.updated) {
165
+ await logger(' ✅ PR body already contains issue reference');
166
+ return { checked: true, updated: false, body: linkResult.body, issueRef: linkResult.issueRef };
167
+ }
168
+
169
+ await logger(` 📝 Updating PR body to link issue #${issueNumber}...`);
170
+
171
+ const fs = (await use('fs')).promises;
172
+ const tempBodyFile = `/tmp/pr-body-update-${prNumber}-${Date.now()}.md`;
173
+ await fs.writeFile(tempBodyFile, linkResult.body);
174
+
175
+ try {
176
+ const updateResult = await command`gh pr edit ${prNumber} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
177
+ await fs.unlink(tempBodyFile).catch(() => {});
178
+
179
+ if (updateResult.code === 0) {
180
+ await logger(` ✅ Updated PR body to include "Fixes ${linkResult.issueRef}"`);
181
+ return { checked: true, updated: true, body: linkResult.body, issueRef: linkResult.issueRef };
182
+ }
183
+
184
+ const error = updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error';
185
+ await logger(` ⚠️ Could not update PR body: ${error}`);
186
+ return { checked: true, updated: false, body: prBody, issueRef: linkResult.issueRef, error };
187
+ } catch (updateError) {
188
+ await fs.unlink(tempBodyFile).catch(() => {});
189
+ throw updateError;
190
+ }
191
+ };
192
+
193
+ export const verifyPullRequestIssueLinkAfterAutoRestart = async ({ prNumber, issueNumber, owner, repo, argv = {}, cleanErrorMessage = error => error.message }) => {
194
+ if (!prNumber) {
195
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }) };
196
+ }
197
+
198
+ await log('🔗 Verifying PR issue link after auto-restart...');
199
+ try {
200
+ return await ensurePullRequestIssueLink({ prNumber, issueNumber, owner, repo, argv });
201
+ } catch (issueLinkError) {
202
+ await log(`⚠️ Could not verify PR issue link after auto-restart: ${cleanErrorMessage(issueLinkError)}`, { level: 'warning' });
203
+ return { checked: false, updated: false, body: '', issueRef: buildIssueReference({ issueNumber, owner, repo, fork: argv.fork }), error: issueLinkError.message };
204
+ }
205
+ };
206
+
130
207
  /**
131
208
  * Detect the CLAUDE.md or .gitkeep commit hash from branch structure when not available in session
132
209
  * This handles continue mode where the commit hash was lost between sessions
@@ -622,52 +699,16 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
622
699
 
623
700
  // Skip PR body update and ready conversion for merged PRs (they can't be edited)
624
701
  if (!isPrMerged) {
625
- // Check if PR body has proper issue linking keywords
626
- let prBody = '';
627
- const prBodyResult = await $`gh pr view ${pr.number} --repo ${owner}/${repo} --json body --jq .body`;
628
- if (prBodyResult.code === 0) {
629
- prBody = prBodyResult.stdout.toString();
630
- const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
631
-
632
- // Use the new GitHub linking detection library to check for valid keywords
633
- // This ensures we only detect actual GitHub-recognized linking keywords
634
- // (fixes, closes, resolves and their variants) in proper format
635
- // See: https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
636
- const hasLinkingKeyword = hasGitHubLinkingKeyword(prBody, issueNumber, argv.fork ? owner : null, argv.fork ? repo : null);
637
-
638
- if (!hasLinkingKeyword) {
639
- await log(` 📝 Updating PR body to link issue #${issueNumber}...`);
640
-
641
- // Add proper issue reference to the PR body
642
- const linkingText = `\n\nFixes ${issueRef}`;
643
- const updatedBody = prBody + linkingText;
644
-
645
- // Use --body-file instead of --body to avoid command-line length limits
646
- // and special character escaping issues that can cause hangs/timeouts
647
- const fs = (await use('fs')).promises;
648
- const tempBodyFile = `/tmp/pr-body-update-${pr.number}-${Date.now()}.md`;
649
- await fs.writeFile(tempBodyFile, updatedBody);
650
-
651
- try {
652
- const updateResult = await $`gh pr edit ${pr.number} --repo ${owner}/${repo} --body-file "${tempBodyFile}"`;
653
-
654
- // Clean up temp file
655
- await fs.unlink(tempBodyFile).catch(() => {});
656
-
657
- if (updateResult.code === 0) {
658
- await log(` ✅ Updated PR body to include "Fixes ${issueRef}"`);
659
- } else {
660
- await log(` ⚠️ Could not update PR body: ${updateResult.stderr ? updateResult.stderr.toString().trim() : 'Unknown error'}`);
661
- }
662
- } catch (updateError) {
663
- // Clean up temp file on error
664
- await fs.unlink(tempBodyFile).catch(() => {});
665
- throw updateError;
666
- }
667
- } else {
668
- await log(' ✅ PR body already contains issue reference');
669
- }
670
- }
702
+ const issueLinkResult = await ensurePullRequestIssueLink({
703
+ prNumber: pr.number,
704
+ issueNumber,
705
+ owner,
706
+ repo,
707
+ argv,
708
+ command: $,
709
+ logger: log,
710
+ });
711
+ const prBody = issueLinkResult.body || '';
671
712
 
672
713
  // Issue #1162: Detect if PR title/description still have auto-generated placeholder content
673
714
  // Track this before cleanup for --auto-restart-on-non-updated-pull-request-description
@@ -709,7 +750,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
709
750
 
710
751
  // Build new description
711
752
  const fs = (await use('fs')).promises;
712
- const issueRef = argv.fork ? `${owner}/${repo}#${issueNumber}` : `#${issueNumber}`;
753
+ const issueRef = buildIssueReference({ issueNumber, owner, repo, fork: argv.fork });
713
754
  const newDescription = `## Summary
714
755
 
715
756
  This pull request implements a solution for ${issueRef}: ${issueTitle}