@link-assistant/hive-mind 0.41.2 ā 0.41.5
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 +35 -0
- package/package.json +1 -1
- package/src/github.lib.mjs +16 -0
- package/src/hive.mjs +3 -4
- package/src/solve.results.lib.mjs +141 -4
- package/src/solve.validation.lib.mjs +3 -4
- package/src/telegram-bot.mjs +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 0.41.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 27bbc44: Add backslash detection and validation in GitHub URLs
|
|
8
|
+
|
|
9
|
+
When users provide URLs with backslashes (e.g., `https://github.com/owner/repo/issues/123\`), the system now properly validates them and provides helpful error messages with auto-corrected URL suggestions. According to RFC 3986, backslash is not a valid character in URL paths.
|
|
10
|
+
|
|
11
|
+
**Changes:**
|
|
12
|
+
|
|
13
|
+
- Enhanced `parseGitHubUrl()` function to detect backslashes in URL paths
|
|
14
|
+
- Updated all validation points (Telegram bot `/solve` and `/hive` commands, CLI `hive` and `solve` commands)
|
|
15
|
+
- Provides user-friendly error messages with corrected URL suggestions
|
|
16
|
+
- Comprehensive test suite for backslash validation scenarios
|
|
17
|
+
|
|
18
|
+
Fixes #923
|
|
19
|
+
|
|
20
|
+
## 0.41.3
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- db8cef7: Fix CLAUDE.md not being deleted in continue mode
|
|
25
|
+
|
|
26
|
+
When a work session completes successfully but the CLAUDE.md commit hash was lost between sessions (e.g., due to session interruption), the system now attempts to detect the CLAUDE.md commit from the branch structure instead of silently skipping cleanup.
|
|
27
|
+
|
|
28
|
+
**Safety Checks (Preventing Issue #617 Recurrence):**
|
|
29
|
+
|
|
30
|
+
1. CLAUDE.md must exist in current branch
|
|
31
|
+
2. Find merge base to isolate PR-only commits
|
|
32
|
+
3. Must have at least 2 commits (CLAUDE.md + actual work)
|
|
33
|
+
4. First commit message must match expected pattern
|
|
34
|
+
5. First commit must ONLY change CLAUDE.md file
|
|
35
|
+
|
|
36
|
+
Fixes #940
|
|
37
|
+
|
|
3
38
|
## 0.41.2
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/package.json
CHANGED
package/src/github.lib.mjs
CHANGED
|
@@ -1107,6 +1107,22 @@ export function parseGitHubUrl(url) {
|
|
|
1107
1107
|
if (normalizedUrl.startsWith('http://')) {
|
|
1108
1108
|
normalizedUrl = normalizedUrl.replace(/^http:\/\//, 'https://');
|
|
1109
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
// Check for backslashes in the URL path (excluding query params and hash)
|
|
1112
|
+
// According to RFC 3986, backslash is not a valid character in URL paths
|
|
1113
|
+
const urlBeforeQueryAndHash = normalizedUrl.split('?')[0].split('#')[0];
|
|
1114
|
+
if (urlBeforeQueryAndHash.includes('\\')) {
|
|
1115
|
+
// Generate suggested URL by replacing backslashes with forward slashes
|
|
1116
|
+
const suggestedUrl = urlBeforeQueryAndHash.replace(/\\/g, '/');
|
|
1117
|
+
const urlAfterPath = normalizedUrl.substring(urlBeforeQueryAndHash.length);
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
valid: false,
|
|
1121
|
+
error: 'Invalid character in URL: backslash (\\) is not allowed in URL paths',
|
|
1122
|
+
suggestion: suggestedUrl + urlAfterPath
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1110
1126
|
// Parse the URL
|
|
1111
1127
|
let urlObj;
|
|
1112
1128
|
try {
|
package/src/hive.mjs
CHANGED
|
@@ -337,10 +337,9 @@ if (githubUrl) {
|
|
|
337
337
|
|
|
338
338
|
if (!parsedUrl.valid) {
|
|
339
339
|
console.error('Error: Invalid GitHub URL format');
|
|
340
|
-
if (parsedUrl.error) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
console.error('Expected: https://github.com/owner or https://github.com/owner/repo');
|
|
340
|
+
if (parsedUrl.error) console.error(` ${parsedUrl.error}`);
|
|
341
|
+
if (parsedUrl.suggestion) console.error(`\nš” Did you mean: ${parsedUrl.suggestion}`);
|
|
342
|
+
console.error('\nExpected: https://github.com/owner or https://github.com/owner/repo');
|
|
344
343
|
console.error('You can use any of these formats:');
|
|
345
344
|
console.error(' - https://github.com/owner');
|
|
346
345
|
console.error(' - https://github.com/owner/repo');
|
|
@@ -50,14 +50,151 @@ const { reportError } = sentryLib;
|
|
|
50
50
|
const githubLinking = await import('./github-linking.lib.mjs');
|
|
51
51
|
const { hasGitHubLinkingKeyword } = githubLinking;
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Detect the CLAUDE.md commit hash from branch structure when not available in session
|
|
55
|
+
* This handles continue mode where the commit hash was lost between sessions
|
|
56
|
+
*
|
|
57
|
+
* Safety checks to prevent Issue #617 (wrong commit revert):
|
|
58
|
+
* 1. Only look at commits on the PR branch (not default branch commits)
|
|
59
|
+
* 2. Verify the commit message matches our expected pattern
|
|
60
|
+
* 3. Verify the commit ONLY adds CLAUDE.md (no other files changed)
|
|
61
|
+
* 4. Verify there are additional commits after it (actual work was done)
|
|
62
|
+
*
|
|
63
|
+
* @param {string} tempDir - The temporary directory with the git repo
|
|
64
|
+
* @param {string} branchName - The PR branch name
|
|
65
|
+
* @returns {string|null} - The detected commit hash or null if not found/safe
|
|
66
|
+
*/
|
|
67
|
+
const detectClaudeMdCommitFromBranch = async (tempDir, branchName) => {
|
|
68
|
+
try {
|
|
69
|
+
await log(' Attempting to detect CLAUDE.md commit from branch structure...', { verbose: true });
|
|
70
|
+
|
|
71
|
+
// First check if CLAUDE.md exists in current branch
|
|
72
|
+
const claudeMdExistsResult = await $({ cwd: tempDir })`git ls-files CLAUDE.md 2>&1`;
|
|
73
|
+
if (claudeMdExistsResult.code !== 0 || !claudeMdExistsResult.stdout || !claudeMdExistsResult.stdout.trim()) {
|
|
74
|
+
await log(' CLAUDE.md does not exist in current branch', { verbose: true });
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Get the default branch to find the fork point
|
|
79
|
+
const defaultBranchResult = await $({ cwd: tempDir })`git symbolic-ref refs/remotes/origin/HEAD 2>&1`;
|
|
80
|
+
let defaultBranch = 'main';
|
|
81
|
+
if (defaultBranchResult.code === 0 && defaultBranchResult.stdout) {
|
|
82
|
+
const match = defaultBranchResult.stdout.toString().match(/refs\/remotes\/origin\/(.+)/);
|
|
83
|
+
if (match) {
|
|
84
|
+
defaultBranch = match[1].trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await log(` Using default branch: ${defaultBranch}`, { verbose: true });
|
|
88
|
+
|
|
89
|
+
// Find the merge base (fork point) between current branch and default branch
|
|
90
|
+
const mergeBaseResult = await $({ cwd: tempDir })`git merge-base origin/${defaultBranch} HEAD 2>&1`;
|
|
91
|
+
if (mergeBaseResult.code !== 0 || !mergeBaseResult.stdout) {
|
|
92
|
+
await log(' Could not find merge base, cannot safely detect CLAUDE.md commit', { verbose: true });
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const mergeBase = mergeBaseResult.stdout.toString().trim();
|
|
96
|
+
await log(` Merge base: ${mergeBase.substring(0, 7)}`, { verbose: true });
|
|
97
|
+
|
|
98
|
+
// Get all commits on the PR branch (commits after the merge base)
|
|
99
|
+
// Format: hash|message|files_changed
|
|
100
|
+
const branchCommitsResult = await $({ cwd: tempDir })`git log ${mergeBase}..HEAD --reverse --format="%H|%s" 2>&1`;
|
|
101
|
+
if (branchCommitsResult.code !== 0 || !branchCommitsResult.stdout) {
|
|
102
|
+
await log(' No commits found on PR branch', { verbose: true });
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const branchCommits = branchCommitsResult.stdout.toString().trim().split('\n').filter(Boolean);
|
|
107
|
+
if (branchCommits.length === 0) {
|
|
108
|
+
await log(' No commits found on PR branch', { verbose: true });
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await log(` Found ${branchCommits.length} commit(s) on PR branch`, { verbose: true });
|
|
113
|
+
|
|
114
|
+
// Safety check: Must have at least 2 commits (CLAUDE.md commit + actual work)
|
|
115
|
+
if (branchCommits.length < 2) {
|
|
116
|
+
await log(' Only 1 commit on branch - not enough commits to safely revert CLAUDE.md', { verbose: true });
|
|
117
|
+
await log(' (Need at least 2 commits: CLAUDE.md initial + actual work)', { verbose: true });
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get the first commit on the PR branch
|
|
122
|
+
const firstCommitLine = branchCommits[0];
|
|
123
|
+
const [firstCommitHash, firstCommitMessage] = firstCommitLine.split('|');
|
|
124
|
+
|
|
125
|
+
await log(` First commit on branch: ${firstCommitHash.substring(0, 7)} - "${firstCommitMessage}"`, { verbose: true });
|
|
126
|
+
|
|
127
|
+
// Safety check: Verify commit message matches expected pattern
|
|
128
|
+
const expectedMessagePatterns = [
|
|
129
|
+
/^Initial commit with task details/i,
|
|
130
|
+
/^Add CLAUDE\.md/i,
|
|
131
|
+
/^CLAUDE\.md/i
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const messageMatches = expectedMessagePatterns.some(pattern => pattern.test(firstCommitMessage));
|
|
135
|
+
if (!messageMatches) {
|
|
136
|
+
await log(' First commit message does not match expected CLAUDE.md pattern', { verbose: true });
|
|
137
|
+
await log(' Expected patterns: "Initial commit with task details...", "Add CLAUDE.md", etc.', { verbose: true });
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Safety check: Verify the commit ONLY adds CLAUDE.md file (no other files)
|
|
142
|
+
const filesChangedResult = await $({ cwd: tempDir })`git diff-tree --no-commit-id --name-only -r ${firstCommitHash} 2>&1`;
|
|
143
|
+
if (filesChangedResult.code !== 0 || !filesChangedResult.stdout) {
|
|
144
|
+
await log(' Could not get files changed in first commit', { verbose: true });
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const filesChanged = filesChangedResult.stdout.toString().trim().split('\n').filter(Boolean);
|
|
149
|
+
await log(` Files changed in first commit: ${filesChanged.join(', ')}`, { verbose: true });
|
|
150
|
+
|
|
151
|
+
// Check if CLAUDE.md is in the files changed
|
|
152
|
+
if (!filesChanged.includes('CLAUDE.md')) {
|
|
153
|
+
await log(' First commit does not include CLAUDE.md', { verbose: true });
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// CRITICAL SAFETY CHECK: Only allow revert if CLAUDE.md is the ONLY file changed
|
|
158
|
+
// This prevents Issue #617 where reverting a commit deleted .gitignore, LICENSE, README.md
|
|
159
|
+
if (filesChanged.length > 1) {
|
|
160
|
+
await log(` ā ļø First commit changes more than just CLAUDE.md (${filesChanged.length} files)`, { verbose: true });
|
|
161
|
+
await log(` Files: ${filesChanged.join(', ')}`, { verbose: true });
|
|
162
|
+
await log(' Refusing to revert to prevent data loss (Issue #617 safety)', { verbose: true });
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// All safety checks passed!
|
|
167
|
+
await log(` ā
Detected CLAUDE.md commit: ${firstCommitHash.substring(0, 7)}`, { verbose: true });
|
|
168
|
+
await log(' ā
Commit only contains CLAUDE.md (safe to revert)', { verbose: true });
|
|
169
|
+
await log(` ā
Branch has ${branchCommits.length - 1} additional commit(s) (work was done)`, { verbose: true });
|
|
170
|
+
|
|
171
|
+
return firstCommitHash;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
reportError(error, {
|
|
174
|
+
context: 'detect_claude_md_commit',
|
|
175
|
+
tempDir,
|
|
176
|
+
branchName,
|
|
177
|
+
operation: 'detect_commit_from_branch_structure'
|
|
178
|
+
});
|
|
179
|
+
await log(` Error detecting CLAUDE.md commit: ${error.message}`, { verbose: true });
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
53
184
|
// Revert the CLAUDE.md commit to restore original state
|
|
54
185
|
export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash = null) => {
|
|
55
186
|
try {
|
|
56
|
-
//
|
|
57
|
-
// This
|
|
187
|
+
// If no commit hash provided, try to detect it from branch structure
|
|
188
|
+
// This handles continue mode where the hash was lost between sessions
|
|
58
189
|
if (!claudeCommitHash) {
|
|
59
|
-
await log(' No CLAUDE.md commit
|
|
60
|
-
|
|
190
|
+
await log(' No CLAUDE.md commit hash from session, attempting to detect from branch...', { verbose: true });
|
|
191
|
+
claudeCommitHash = await detectClaudeMdCommitFromBranch(tempDir, branchName);
|
|
192
|
+
|
|
193
|
+
if (!claudeCommitHash) {
|
|
194
|
+
await log(' Could not safely detect CLAUDE.md commit to revert', { verbose: true });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await log(` Detected CLAUDE.md commit: ${claudeCommitHash.substring(0, 7)}`, { verbose: true });
|
|
61
198
|
}
|
|
62
199
|
|
|
63
200
|
await log(formatAligned('š', 'Cleanup:', 'Reverting CLAUDE.md commit'));
|
|
@@ -66,10 +66,9 @@ export const validateGitHubUrl = (issueUrl) => {
|
|
|
66
66
|
|
|
67
67
|
if (!parsedUrl.valid) {
|
|
68
68
|
console.error('Error: Invalid GitHub URL format');
|
|
69
|
-
if (parsedUrl.error) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.error(' Please provide a valid GitHub issue or pull request URL');
|
|
69
|
+
if (parsedUrl.error) console.error(` ${parsedUrl.error}`);
|
|
70
|
+
if (parsedUrl.suggestion) console.error(`\nš” Did you mean: ${parsedUrl.suggestion}`);
|
|
71
|
+
console.error('\n Please provide a valid GitHub issue or pull request URL');
|
|
73
72
|
console.error(' Examples:');
|
|
74
73
|
console.error(' https://github.com/owner/repo/issues/123 (issue)');
|
|
75
74
|
console.error(' https://github.com/owner/repo/pull/456 (pull request)');
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -638,7 +638,8 @@ function validateGitHubUrl(args, options = {}) {
|
|
|
638
638
|
if (!parsed.valid) {
|
|
639
639
|
return {
|
|
640
640
|
valid: false,
|
|
641
|
-
error: parsed.error || 'Invalid GitHub URL'
|
|
641
|
+
error: parsed.error || 'Invalid GitHub URL',
|
|
642
|
+
suggestion: parsed.suggestion
|
|
642
643
|
};
|
|
643
644
|
}
|
|
644
645
|
|
|
@@ -987,7 +988,12 @@ bot.command(/^solve$/i, async (ctx) => {
|
|
|
987
988
|
|
|
988
989
|
const validation = validateGitHubUrl(userArgs);
|
|
989
990
|
if (!validation.valid) {
|
|
990
|
-
|
|
991
|
+
let errorMsg = `ā ${validation.error}`;
|
|
992
|
+
if (validation.suggestion) {
|
|
993
|
+
errorMsg += `\n\nš” Did you mean: \`${validation.suggestion}\``;
|
|
994
|
+
}
|
|
995
|
+
errorMsg += '\n\nExample: `/solve https://github.com/owner/repo/issues/123`\n\nOr reply to a message containing a GitHub link with `/solve`';
|
|
996
|
+
await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
991
997
|
return;
|
|
992
998
|
}
|
|
993
999
|
|
|
@@ -1136,7 +1142,12 @@ bot.command(/^hive$/i, async (ctx) => {
|
|
|
1136
1142
|
exampleUrl: 'https://github.com/owner/repo'
|
|
1137
1143
|
});
|
|
1138
1144
|
if (!validation.valid) {
|
|
1139
|
-
|
|
1145
|
+
let errorMsg = `ā ${validation.error}`;
|
|
1146
|
+
if (validation.suggestion) {
|
|
1147
|
+
errorMsg += `\n\nš” Did you mean: \`${validation.suggestion}\``;
|
|
1148
|
+
}
|
|
1149
|
+
errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
|
|
1150
|
+
await ctx.reply(errorMsg, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
1140
1151
|
return;
|
|
1141
1152
|
}
|
|
1142
1153
|
|