@link-assistant/hive-mind 0.41.2 → 0.41.3

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,23 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.41.3
4
+
5
+ ### Patch Changes
6
+
7
+ - db8cef7: Fix CLAUDE.md not being deleted in continue mode
8
+
9
+ 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.
10
+
11
+ **Safety Checks (Preventing Issue #617 Recurrence):**
12
+
13
+ 1. CLAUDE.md must exist in current branch
14
+ 2. Find merge base to isolate PR-only commits
15
+ 3. Must have at least 2 commits (CLAUDE.md + actual work)
16
+ 4. First commit message must match expected pattern
17
+ 5. First commit must ONLY change CLAUDE.md file
18
+
19
+ Fixes #940
20
+
3
21
  ## 0.41.2
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.41.2",
3
+ "version": "0.41.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- // Only revert if we have the commit hash from this session
57
- // This prevents reverting the wrong commit in continue mode
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 to revert (not created in this session)', { verbose: true });
60
- return;
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'));