bobs-workshop 0.3.3 → 3.1.0

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.
Files changed (200) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +199 -210
  3. package/bin/bobs-workshop.js +109 -0
  4. package/config/agents.json +27 -0
  5. package/dist/plugins/bobs-workshop.js +34 -0
  6. package/dist/tools/background-agent/cancel.d.ts +3 -0
  7. package/dist/tools/background-agent/cancel.d.ts.map +1 -0
  8. package/dist/tools/background-agent/cancel.js +52 -0
  9. package/dist/tools/background-agent/concurrency.d.ts +15 -0
  10. package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
  11. package/dist/tools/background-agent/concurrency.js +61 -0
  12. package/dist/tools/background-agent/index.d.ts +8 -0
  13. package/dist/tools/background-agent/index.d.ts.map +1 -0
  14. package/dist/tools/background-agent/index.js +7 -0
  15. package/dist/tools/background-agent/launch.d.ts +6 -0
  16. package/dist/tools/background-agent/launch.d.ts.map +1 -0
  17. package/dist/tools/background-agent/launch.js +33 -0
  18. package/dist/tools/background-agent/list.d.ts +7 -0
  19. package/dist/tools/background-agent/list.d.ts.map +1 -0
  20. package/dist/tools/background-agent/list.js +40 -0
  21. package/dist/tools/background-agent/manager.d.ts +29 -0
  22. package/dist/tools/background-agent/manager.d.ts.map +1 -0
  23. package/dist/tools/background-agent/manager.js +377 -0
  24. package/dist/tools/background-agent/output.d.ts +3 -0
  25. package/dist/tools/background-agent/output.d.ts.map +1 -0
  26. package/dist/tools/background-agent/output.js +41 -0
  27. package/dist/tools/background-agent/types.d.ts +46 -0
  28. package/dist/tools/background-agent/types.d.ts.map +1 -0
  29. package/dist/tools/background-agent/types.js +1 -0
  30. package/dist/tools/index.d.ts +9 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +8 -0
  33. package/dist/tools/manual/index.d.ts +3 -0
  34. package/dist/tools/manual/index.d.ts.map +1 -0
  35. package/dist/tools/manual/index.js +2 -0
  36. package/dist/tools/manual/manual-update.d.ts +4 -0
  37. package/dist/tools/manual/manual-update.d.ts.map +1 -0
  38. package/dist/tools/manual/manual-update.js +190 -0
  39. package/dist/tools/manual/verify-manual.d.ts +4 -0
  40. package/dist/tools/manual/verify-manual.d.ts.map +1 -0
  41. package/dist/tools/manual/verify-manual.js +46 -0
  42. package/package.json +34 -66
  43. package/postinstall.js +190 -0
  44. package/src/agents/alice.md +466 -0
  45. package/src/agents/bob-rev.md +493 -0
  46. package/src/agents/bob-send.md +277 -0
  47. package/src/agents/bob.md +442 -0
  48. package/src/agents/trace.md +451 -0
  49. package/src/plugins/bobs-workshop.ts +45 -0
  50. package/src/skills/api-patterns/SKILL.md +376 -0
  51. package/src/skills/architecture/SKILL.md +271 -0
  52. package/src/skills/bobs-workshop/performance/icon.svg +3 -0
  53. package/src/skills/brainstorming/SKILL.md +210 -0
  54. package/src/skills/clean-code/SKILL.md +151 -0
  55. package/src/skills/code-review-checklist/SKILL.md +220 -0
  56. package/src/skills/database-design/SKILL.md +271 -0
  57. package/src/skills/exploration/SKILL.md +257 -0
  58. package/src/skills/frontend-ui-ux/SKILL.md +78 -0
  59. package/src/skills/git-master/SKILL.md +1105 -0
  60. package/src/skills/performance/SKILL.md +144 -0
  61. package/src/skills/performance/icon.svg +3 -0
  62. package/src/skills/plan-writing/SKILL.md +225 -0
  63. package/src/skills/security/SKILL.md +410 -0
  64. package/src/skills/simplification/SKILL.md +238 -0
  65. package/src/skills/systematic-debugging/SKILL.md +175 -0
  66. package/src/skills/testing-patterns/SKILL.md +305 -0
  67. package/src/skills/verification/SKILL.md +286 -0
  68. package/src/tools/background-agent/cancel.ts +67 -0
  69. package/src/tools/background-agent/concurrency.ts +71 -0
  70. package/src/tools/background-agent/index.ts +7 -0
  71. package/src/tools/background-agent/launch.ts +39 -0
  72. package/src/tools/background-agent/list.ts +50 -0
  73. package/src/tools/background-agent/manager.ts +455 -0
  74. package/src/tools/background-agent/output.ts +57 -0
  75. package/src/tools/background-agent/types.ts +55 -0
  76. package/src/tools/index.ts +8 -0
  77. package/src/tools/manual/index.ts +2 -0
  78. package/src/tools/manual/manual-update.ts +197 -0
  79. package/src/tools/manual/verify-manual.ts +55 -0
  80. package/uninstall.js +64 -0
  81. package/Claude.md +0 -162
  82. package/bin/bobs-mcp-server.js +0 -11
  83. package/bin/bobs-mcp.js +0 -130
  84. package/dist/api/taskLogger.js +0 -106
  85. package/dist/api/taskLogger.js.map +0 -1
  86. package/dist/cli/checker.js +0 -401
  87. package/dist/cli/checker.js.map +0 -1
  88. package/dist/cli/cleanup.js +0 -131
  89. package/dist/cli/cleanup.js.map +0 -1
  90. package/dist/cli/debug.js +0 -157
  91. package/dist/cli/debug.js.map +0 -1
  92. package/dist/cli/health.js +0 -97
  93. package/dist/cli/health.js.map +0 -1
  94. package/dist/cli/setup.js +0 -81
  95. package/dist/cli/setup.js.map +0 -1
  96. package/dist/cli/workshop.js +0 -42
  97. package/dist/cli/workshop.js.map +0 -1
  98. package/dist/dashboard/server.js +0 -1203
  99. package/dist/dashboard/server.js.map +0 -1
  100. package/dist/index.js +0 -960
  101. package/dist/index.js.map +0 -1
  102. package/dist/prompts/architect.js +0 -221
  103. package/dist/prompts/architect.js.map +0 -1
  104. package/dist/prompts/debugger.js +0 -257
  105. package/dist/prompts/debugger.js.map +0 -1
  106. package/dist/prompts/engineer.js +0 -249
  107. package/dist/prompts/engineer.js.map +0 -1
  108. package/dist/prompts/orchestrator.js +0 -304
  109. package/dist/prompts/orchestrator.js.map +0 -1
  110. package/dist/prompts/reviewer.js +0 -289
  111. package/dist/prompts/reviewer.js.map +0 -1
  112. package/dist/services/activitySummarizer.js +0 -388
  113. package/dist/services/activitySummarizer.js.map +0 -1
  114. package/dist/services/changeValidator.js +0 -396
  115. package/dist/services/changeValidator.js.map +0 -1
  116. package/dist/services/claudeOrchestrator.js +0 -343
  117. package/dist/services/claudeOrchestrator.js.map +0 -1
  118. package/dist/services/fileMonitor.js +0 -250
  119. package/dist/services/fileMonitor.js.map +0 -1
  120. package/dist/services/implementationSummarizer.js +0 -306
  121. package/dist/services/implementationSummarizer.js.map +0 -1
  122. package/dist/services/liveMonitor.js +0 -315
  123. package/dist/services/liveMonitor.js.map +0 -1
  124. package/dist/services/mcpAuditLogger.js +0 -104
  125. package/dist/services/mcpAuditLogger.js.map +0 -1
  126. package/dist/services/mcpLogger.js +0 -223
  127. package/dist/services/mcpLogger.js.map +0 -1
  128. package/dist/services/tmuxManager.js +0 -541
  129. package/dist/services/tmuxManager.js.map +0 -1
  130. package/dist/tools/approvalTools.js +0 -244
  131. package/dist/tools/approvalTools.js.map +0 -1
  132. package/dist/tools/autoDebugger.js +0 -147
  133. package/dist/tools/autoDebugger.js.map +0 -1
  134. package/dist/tools/cleanupService.js +0 -221
  135. package/dist/tools/cleanupService.js.map +0 -1
  136. package/dist/tools/dashboardTools.js +0 -342
  137. package/dist/tools/dashboardTools.js.map +0 -1
  138. package/dist/tools/developmentNudges.js +0 -336
  139. package/dist/tools/developmentNudges.js.map +0 -1
  140. package/dist/tools/gitTools.js +0 -741
  141. package/dist/tools/gitTools.js.map +0 -1
  142. package/dist/tools/orchestratorTools.js +0 -832
  143. package/dist/tools/orchestratorTools.js.map +0 -1
  144. package/dist/tools/searchCache.js +0 -64
  145. package/dist/tools/searchCache.js.map +0 -1
  146. package/dist/tools/searchTools.js +0 -1107
  147. package/dist/tools/searchTools.js.map +0 -1
  148. package/dist/tools/semgrep-patterns.js +0 -296
  149. package/dist/tools/semgrep-patterns.js.map +0 -1
  150. package/dist/tools/specTools.js +0 -332
  151. package/dist/tools/specTools.js.map +0 -1
  152. package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
  153. package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
  154. package/dist/tools/structural/cache.js +0 -226
  155. package/dist/tools/structural/cache.js.map +0 -1
  156. package/dist/tools/structural/engines/python/index.js +0 -118
  157. package/dist/tools/structural/engines/python/index.js.map +0 -1
  158. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
  159. package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
  160. package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
  161. package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
  162. package/dist/tools/structural/engines/typescript/index.js +0 -381
  163. package/dist/tools/structural/engines/typescript/index.js.map +0 -1
  164. package/dist/tools/structural/engines/typescript/utils.js +0 -279
  165. package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
  166. package/dist/tools/structural/index.js +0 -248
  167. package/dist/tools/structural/index.js.map +0 -1
  168. package/dist/tools/structural/types.js +0 -18
  169. package/dist/tools/structural/types.js.map +0 -1
  170. package/dist/tools/tmuxTools.js +0 -100
  171. package/dist/tools/tmuxTools.js.map +0 -1
  172. package/dist/tools/workRecorder.js +0 -215
  173. package/dist/tools/workRecorder.js.map +0 -1
  174. package/dist/tools/worktreeTools.js +0 -705
  175. package/dist/tools/worktreeTools.js.map +0 -1
  176. package/dist/utils/__tests__/integration.test.js +0 -57
  177. package/dist/utils/__tests__/integration.test.js.map +0 -1
  178. package/dist/utils/__tests__/serverDetection.test.js +0 -151
  179. package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
  180. package/dist/utils/errorHandling.js +0 -336
  181. package/dist/utils/errorHandling.js.map +0 -1
  182. package/dist/utils/processManager.js +0 -172
  183. package/dist/utils/processManager.js.map +0 -1
  184. package/dist/utils/reliability.js +0 -263
  185. package/dist/utils/reliability.js.map +0 -1
  186. package/dist/utils/responseFormatter.js +0 -250
  187. package/dist/utils/responseFormatter.js.map +0 -1
  188. package/dist/utils/serverDetection.js +0 -133
  189. package/dist/utils/serverDetection.js.map +0 -1
  190. package/dist/utils/specMigration.js +0 -105
  191. package/dist/utils/specMigration.js.map +0 -1
  192. package/dist/validation/schemas.js +0 -299
  193. package/dist/validation/schemas.js.map +0 -1
  194. package/public/.well-known/mcp/manifest.json +0 -473
  195. package/public/index.html +0 -3157
  196. package/public/index.html.backup +0 -2805
  197. package/public/index.html.backup2 +0 -1292
  198. package/scripts/cleanup-system-logs.ts +0 -121
  199. package/scripts/init-workspace.js +0 -63
  200. package/scripts/install-search-tools.js +0 -116
@@ -1,741 +0,0 @@
1
- // src/tools/gitTools.ts
2
- import { exec } from "child_process";
3
- import { promisify } from "util";
4
- import { z } from "zod";
5
- import path from "path";
6
- import fs from "fs";
7
- import os from "os";
8
- import { specGetHandler, specUpdateHandler } from "./specTools.js";
9
- const execAsync = promisify(exec);
10
- // Find worktree path associated with a SPEC
11
- export async function findWorktreeForSpec(specId) {
12
- try {
13
- console.log(`Finding worktree for SPEC: ${specId}`);
14
- // PRIMARY: Read SPEC metadata for worktree information
15
- try {
16
- const spec = await specGetHandler({ spec_id: specId });
17
- if (spec.worktree && spec.worktree.path && spec.worktree.status === 'active') {
18
- console.log(`Found worktree in SPEC metadata: ${spec.worktree.path}`);
19
- // Verify the path actually exists
20
- if (fs.existsSync(spec.worktree.path)) {
21
- console.log(`✅ SPEC metadata worktree verified: ${spec.worktree.path}`);
22
- return spec.worktree.path;
23
- }
24
- else {
25
- console.warn(`⚠️ SPEC metadata has path ${spec.worktree.path} but it doesn't exist on filesystem`);
26
- // Continue to fallback methods
27
- }
28
- }
29
- else {
30
- console.log(`SPEC metadata doesn't have active worktree (status: ${spec.worktree?.status || 'missing'})`);
31
- }
32
- }
33
- catch (specError) {
34
- console.warn('Could not read SPEC metadata, falling back to heuristic:', specError);
35
- }
36
- // FALLBACK 1: Use git worktree list with pattern matching
37
- const { stdout } = await execAsync('git worktree list --porcelain');
38
- console.log(`Git worktree list output:\n${stdout}`);
39
- if (!stdout.trim()) {
40
- console.log('No worktrees found');
41
- return null;
42
- }
43
- const worktrees = stdout.trim().split('\n\n');
44
- console.log(`Found ${worktrees.length} worktrees`);
45
- for (const worktreeInfo of worktrees) {
46
- const lines = worktreeInfo.split('\n');
47
- const worktreePath = lines[0].replace('worktree ', '');
48
- console.log(`Checking worktree path: ${worktreePath}`);
49
- // Skip the main repository worktree
50
- if (worktreePath === process.cwd()) {
51
- console.log(`Skipping main repository: ${worktreePath}`);
52
- continue;
53
- }
54
- // Check if this worktree is in our managed worktrees directory
55
- const expectedWorktreePath = path.resolve(process.cwd(), `.bob/worktrees`);
56
- if (worktreePath.startsWith(expectedWorktreePath)) {
57
- console.log(`Found managed worktree: ${worktreePath}`);
58
- // Extract branch name from worktree path and match against SPEC
59
- const branchName = path.basename(worktreePath);
60
- // Check if this branch/worktree is associated with our SPEC
61
- // Look for the feature branch pattern that includes the SPEC
62
- if (branchName.includes('feature/') || worktreePath.includes(specId)) {
63
- if (fs.existsSync(worktreePath)) {
64
- console.log(`✅ Found valid worktree for SPEC ${specId}: ${worktreePath}`);
65
- return worktreePath;
66
- }
67
- else {
68
- console.log(`Worktree path registered but does not exist: ${worktreePath}`);
69
- }
70
- }
71
- }
72
- }
73
- // FALLBACK 2: try to find worktree by SPEC pattern in our managed directory
74
- const expectedPath = path.resolve(process.cwd(), `.bob/worktrees/feature/${specId}`);
75
- if (fs.existsSync(expectedPath)) {
76
- console.log(`✅ Found worktree by SPEC pattern: ${expectedPath}`);
77
- return expectedPath;
78
- }
79
- console.log('No suitable worktree found for this SPEC');
80
- return null;
81
- }
82
- catch (error) {
83
- console.warn('Error finding worktree for SPEC:', error);
84
- return null;
85
- }
86
- }
87
- // Execute git command in a specific directory
88
- async function execAsyncInDir(command, cwd) {
89
- return execAsync(command, { cwd });
90
- }
91
- export const AutoGitCommitInput = z.object({
92
- spec_id: z.string(),
93
- files_changed: z.array(z.string()),
94
- task_id: z.string().optional(),
95
- action_description: z.string().describe("Brief description of what was implemented"),
96
- worktree_path: z.string().optional().describe("Optional worktree directory path to run git commands in")
97
- });
98
- export const AutoGitMergeInput = z.object({
99
- spec_id: z.string(),
100
- target_branch: z.string().default("main"),
101
- cleanup_worktree: z.boolean().default(true)
102
- });
103
- export const GitStatusOutput = z.object({
104
- current_branch: z.string(),
105
- is_clean: z.boolean(),
106
- staged_files: z.array(z.string()),
107
- unstaged_files: z.array(z.string()),
108
- untracked_files: z.array(z.string())
109
- });
110
- // Generate smart commit message based on SPEC and changes
111
- export async function generateCommitMessage(input) {
112
- const validated = AutoGitCommitInput.parse(input);
113
- try {
114
- // Get SPEC data for context
115
- const spec = await specGetHandler({ spec_id: validated.spec_id });
116
- // Analyze files changed to determine type of change
117
- const changedFiles = validated.files_changed;
118
- const isNewFeature = changedFiles.some(file => file.includes('/components/') ||
119
- file.includes('/pages/') ||
120
- file.includes('/features/'));
121
- const isBackend = changedFiles.some(file => file.includes('/api/') ||
122
- file.includes('/server/') ||
123
- file.includes('/services/') ||
124
- file.includes('/tools/'));
125
- const isConfig = changedFiles.some(file => file.includes('package.json') ||
126
- file.includes('.json') ||
127
- file.includes('.config.') ||
128
- file.includes('.env'));
129
- const isTest = changedFiles.some(file => file.includes('.test.') ||
130
- file.includes('.spec.') ||
131
- file.includes('/tests/'));
132
- const isDocs = changedFiles.some(file => file.includes('.md') ||
133
- file.includes('/docs/'));
134
- // Determine commit type
135
- let commitType = 'feat';
136
- if (validated.action_description.toLowerCase().includes('fix')) {
137
- commitType = 'fix';
138
- }
139
- else if (isTest) {
140
- commitType = 'test';
141
- }
142
- else if (isDocs) {
143
- commitType = 'docs';
144
- }
145
- else if (isConfig) {
146
- commitType = 'chore';
147
- }
148
- else if (validated.action_description.toLowerCase().includes('refactor')) {
149
- commitType = 'refactor';
150
- }
151
- // Extract key features from SPEC title
152
- const specTitle = spec.title || 'development work';
153
- const featureName = specTitle.toLowerCase()
154
- .replace(/[^a-z0-9\s]/g, '')
155
- .split(' ')
156
- .slice(0, 3)
157
- .join(' ');
158
- // Create descriptive commit message
159
- const scope = isBackend ? 'backend' : isNewFeature ? 'frontend' : undefined;
160
- const scopeStr = scope ? `(${scope})` : '';
161
- const description = validated.action_description
162
- .toLowerCase()
163
- .replace(/^(implement|add|create|update|fix)\s*/i, '')
164
- .trim();
165
- // Build commit message parts
166
- const subject = `${commitType}${scopeStr}: ${description}`;
167
- // Add body with more details
168
- const body = [
169
- `- ${validated.action_description}`,
170
- validated.task_id ? `- Task: ${validated.task_id}` : null,
171
- `- SPEC: ${validated.spec_id}`,
172
- changedFiles.length <= 5 ? `- Files: ${changedFiles.join(', ')}` : `- Modified ${changedFiles.length} files`
173
- ].filter(Boolean).join('\n');
174
- const footer = [
175
- '',
176
- '🤖 Generated with [Claude Code](https://claude.ai/code)',
177
- '',
178
- 'Co-Authored-By: Claude <noreply@anthropic.com>'
179
- ].join('\n');
180
- return `${subject}\n\n${body}\n${footer}`;
181
- }
182
- catch (error) {
183
- // Fallback to simple message if SPEC lookup fails
184
- return `${validated.action_description}\n\n🤖 Generated with [Claude Code](https://claude.ai/code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>`;
185
- }
186
- }
187
- // Get current git status
188
- export async function getGitStatus() {
189
- try {
190
- // Get current branch
191
- const { stdout: branchOutput } = await execAsync('git branch --show-current');
192
- const currentBranch = branchOutput.trim();
193
- // Get status
194
- const { stdout: statusOutput } = await execAsync('git status --porcelain');
195
- const statusLines = statusOutput.trim().split('\n').filter(line => line.length > 0);
196
- const stagedFiles = [];
197
- const unstagedFiles = [];
198
- const untrackedFiles = [];
199
- for (const line of statusLines) {
200
- const status = line.substring(0, 2);
201
- const filePath = line.substring(3);
202
- if (status[0] !== ' ' && status[0] !== '?') {
203
- stagedFiles.push(filePath);
204
- }
205
- if (status[1] !== ' ' && status[1] !== '?') {
206
- unstagedFiles.push(filePath);
207
- }
208
- if (status === '??') {
209
- untrackedFiles.push(filePath);
210
- }
211
- }
212
- return {
213
- current_branch: currentBranch,
214
- is_clean: statusLines.length === 0,
215
- staged_files: stagedFiles,
216
- unstaged_files: unstagedFiles,
217
- untracked_files: untrackedFiles
218
- };
219
- }
220
- catch (error) {
221
- throw new Error(`Failed to get git status: ${error}`);
222
- }
223
- }
224
- // Auto-commit changes with generated message
225
- export async function autoCommitChanges(input) {
226
- const validated = AutoGitCommitInput.parse(input);
227
- try {
228
- // Determine working directory (worktree or current directory)
229
- const workingDir = validated.worktree_path || process.cwd();
230
- console.log(`autoCommitChanges running in: ${workingDir}`);
231
- // Get git status in the working directory
232
- const { stdout: statusOutput } = await execAsyncInDir('git status --porcelain', workingDir);
233
- const hasChanges = statusOutput.trim().length > 0;
234
- if (!hasChanges) {
235
- // No changes to commit - return early with current HEAD
236
- const { stdout: hashOutput } = await execAsyncInDir('git rev-parse HEAD', workingDir);
237
- const commitHash = hashOutput.trim().substring(0, 8);
238
- console.log('No changes to commit - working tree is clean');
239
- return {
240
- commit_hash: commitHash,
241
- message: 'No changes to commit',
242
- files_committed: [],
243
- git_output: 'Working tree clean - no commit needed'
244
- };
245
- }
246
- // Stage files if provided
247
- if (validated.files_changed.length > 0) {
248
- for (const file of validated.files_changed) {
249
- try {
250
- await execAsyncInDir(`git add "${file}"`, workingDir);
251
- }
252
- catch (error) {
253
- console.log(`Warning: Could not stage file ${file}:`, error);
254
- }
255
- }
256
- }
257
- else {
258
- // Stage all changes if no specific files provided
259
- await execAsyncInDir('git add -A', workingDir);
260
- }
261
- // Check if anything was actually staged after git add operations
262
- const { stdout: stagedCheck } = await execAsyncInDir('git diff --cached --name-only', workingDir);
263
- const hasStagedFiles = stagedCheck.trim().length > 0;
264
- if (!hasStagedFiles) {
265
- // Nothing was staged (files already committed or don't exist)
266
- const { stdout: hashOutput } = await execAsyncInDir('git rev-parse HEAD', workingDir);
267
- const commitHash = hashOutput.trim().substring(0, 8);
268
- console.log('No files staged for commit - specified files already committed or do not exist');
269
- return {
270
- commit_hash: commitHash,
271
- message: 'No files staged - already committed',
272
- files_committed: [],
273
- git_output: 'Nothing to commit - specified files already committed'
274
- };
275
- }
276
- // Generate commit message
277
- const commitMessage = await generateCommitMessage(validated);
278
- // Write commit message to temporary file to handle multiline messages properly
279
- // Heredoc syntax doesn't work with Node.js execAsync
280
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bob-commit-'));
281
- const tmpFile = path.join(tmpDir, 'commit-msg.txt');
282
- fs.writeFileSync(tmpFile, commitMessage, 'utf8');
283
- let stdout;
284
- try {
285
- // Create commit using -F to read message from file
286
- const result = await execAsyncInDir(`git commit -F "${tmpFile}"`, workingDir);
287
- stdout = result.stdout;
288
- }
289
- finally {
290
- // Clean up temp file
291
- fs.rmSync(tmpDir, { recursive: true, force: true });
292
- }
293
- // Get commit hash
294
- const { stdout: hashOutput } = await execAsyncInDir('git rev-parse HEAD', workingDir);
295
- const commitHash = hashOutput.trim().substring(0, 8);
296
- // Log the commit in the SPEC
297
- await specUpdateHandler({
298
- spec_id: validated.spec_id,
299
- execution_log: {
300
- timestamp: new Date().toISOString(),
301
- engineer: "BOB_GIT_AUTOMATION",
302
- action: "auto_commit",
303
- task_id: validated.task_id || "AUTO-COMMIT",
304
- files_changed: validated.files_changed,
305
- commit_hash: commitHash,
306
- note: `Auto-committed: ${validated.action_description}`
307
- }
308
- });
309
- return {
310
- commit_hash: commitHash,
311
- message: commitMessage,
312
- files_committed: validated.files_changed,
313
- git_output: stdout.trim()
314
- };
315
- }
316
- catch (error) {
317
- throw new Error(`Failed to auto-commit changes: ${error}`);
318
- }
319
- }
320
- // Auto-merge worktree to main and cleanup
321
- export async function autoMergeAndCleanup(input) {
322
- const validated = AutoGitMergeInput.parse(input);
323
- try {
324
- console.log(`🔧 [FIXED VERSION] autoMergeAndCleanup called for SPEC: ${validated.spec_id}`);
325
- // Get SPEC for context
326
- const spec = await specGetHandler({ spec_id: validated.spec_id });
327
- // Get current repository state
328
- const gitStatus = await getGitStatus();
329
- const currentBranch = gitStatus.current_branch;
330
- console.log(`Current branch: ${currentBranch}, target branch: ${validated.target_branch}`);
331
- // Find the worktree associated with this SPEC
332
- const worktreePath = await findWorktreeForSpec(validated.spec_id);
333
- if (worktreePath) {
334
- // Traditional worktree workflow
335
- console.log(`Found worktree for SPEC ${validated.spec_id} at: ${worktreePath}`);
336
- // Get git status from the worktree using -z flag for null-byte separation
337
- // This is the best practice for parsing (avoids issues with filenames containing spaces)
338
- const { stdout: statusOutput } = await execAsyncInDir('git status --porcelain -z', worktreePath);
339
- const { stdout: branchOutput } = await execAsyncInDir('git branch --show-current', worktreePath);
340
- const worktreeBranch = branchOutput.trim();
341
- // Check for uncommitted changes
342
- if (statusOutput.trim()) {
343
- console.log(`Worktree has uncommitted changes, analyzing...`);
344
- // Parse uncommitted files from git status --porcelain -z format
345
- // -z uses null-byte separators instead of newlines, making parsing reliable
346
- // Format is: "XY filename\0" where X and Y are status codes (2 chars) + 1 space
347
- const uncommittedFiles = statusOutput
348
- .split('\0') // Split on null bytes
349
- .filter(line => line.length > 0) // Remove empty entries
350
- .map(line => {
351
- // First 3 chars are status codes + space (XY ), rest is filename
352
- const fileName = line.substring(3);
353
- // Handle renamed files (contains ->)
354
- const arrowIndex = fileName.indexOf(' -> ');
355
- return arrowIndex > -1 ? fileName.substring(arrowIndex + 4) : fileName;
356
- });
357
- // Define patterns for files that are safe to ignore (build artifacts, dependencies)
358
- const ignorablePatterns = [
359
- /^node_modules\//,
360
- /^dist\//,
361
- /^build\//,
362
- /^\.next\//,
363
- /^out\//,
364
- /^coverage\//,
365
- /^\.cache\//,
366
- /\.log$/,
367
- /\.lock$/,
368
- /^\.env\.local$/
369
- ];
370
- // Categorize files
371
- const importantFiles = uncommittedFiles.filter(file => !ignorablePatterns.some(pattern => pattern.test(file)));
372
- if (importantFiles.length > 0) {
373
- // There are important uncommitted changes - fail gracefully
374
- throw new Error(`Worktree has ${importantFiles.length} uncommitted file(s) that need attention:\n` +
375
- importantFiles.slice(0, 5).map(f => ` - ${f}`).join('\n') +
376
- (importantFiles.length > 5 ? `\n ... and ${importantFiles.length - 5} more` : '') +
377
- `\n\nPlease commit or stash these changes before deploying.`);
378
- }
379
- // Only ignorable files (node_modules, dist, etc.) - auto-stash them
380
- console.log(`Only build artifacts/dependencies changed (${uncommittedFiles.length} files). Auto-stashing...`);
381
- try {
382
- await execAsyncInDir('git stash push -u -m "Auto-stash before merge (build artifacts)"', worktreePath);
383
- console.log('✅ Auto-stashed build artifacts');
384
- }
385
- catch (stashError) {
386
- console.warn('Warning: Could not stash changes, but proceeding anyway:', stashError);
387
- // Don't fail - these are ignorable files
388
- }
389
- }
390
- console.log(`Worktree ready on branch ${worktreeBranch}, proceeding with merge to ${validated.target_branch}`);
391
- // Switch to target branch from main repository (not worktree)
392
- await execAsync(`git checkout ${validated.target_branch}`);
393
- // Check if target branch is clean
394
- const targetBranchStatus = await getGitStatus();
395
- if (!targetBranchStatus.is_clean) {
396
- throw new Error(`Target branch ${validated.target_branch} is not clean. Please clean it before merging.`);
397
- }
398
- // Pull latest changes in target branch
399
- try {
400
- await execAsync(`git pull origin ${validated.target_branch}`);
401
- }
402
- catch (error) {
403
- console.log('Warning: Could not pull latest changes:', error);
404
- }
405
- // Merge the feature branch using temp file (no heredoc)
406
- const mergeMessage = `Merge ${worktreeBranch}: ${spec.title}\n\n🤖 Generated with [Claude Code](https://claude.ai/code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>`;
407
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bob-merge-'));
408
- const tmpFile = path.join(tmpDir, 'merge-msg.txt');
409
- fs.writeFileSync(tmpFile, mergeMessage, 'utf8');
410
- try {
411
- await execAsync(`git merge ${worktreeBranch} -F "${tmpFile}"`);
412
- }
413
- finally {
414
- fs.rmSync(tmpDir, { recursive: true, force: true });
415
- }
416
- // Get merge commit hash
417
- const { stdout: hashOutput } = await execAsync('git rev-parse HEAD');
418
- const mergeCommitHash = hashOutput.trim().substring(0, 8);
419
- let cleanupResult = null;
420
- // Cleanup worktree if requested
421
- if (validated.cleanup_worktree) {
422
- try {
423
- // Remove the worktree first
424
- console.log(`Removing worktree: ${worktreePath}`);
425
- await execAsync(`git worktree remove ${worktreePath}`);
426
- // Delete the feature branch
427
- await execAsync(`git branch -d ${worktreeBranch}`);
428
- // Try to remove remote branch if it exists
429
- try {
430
- await execAsync(`git push origin --delete ${worktreeBranch}`);
431
- }
432
- catch (error) {
433
- console.log('Note: Could not delete remote branch (may not exist):', error);
434
- }
435
- cleanupResult = {
436
- worktree_removed: worktreePath,
437
- branch_deleted: worktreeBranch,
438
- remote_cleanup_attempted: true
439
- };
440
- }
441
- catch (error) {
442
- console.log('Warning: Could not cleanup worktree and branch:', error);
443
- cleanupResult = {
444
- worktree_removed: null,
445
- branch_deleted: null,
446
- cleanup_error: error instanceof Error ? error.message : String(error)
447
- };
448
- }
449
- }
450
- // Log the merge in the SPEC
451
- await specUpdateHandler({
452
- spec_id: validated.spec_id,
453
- execution_log: {
454
- timestamp: new Date().toISOString(),
455
- engineer: "BOB_GIT_AUTOMATION",
456
- action: "auto_merge",
457
- task_id: "MERGE-COMPLETE",
458
- files_changed: [],
459
- commit_hash: mergeCommitHash,
460
- note: `Auto-merged ${worktreeBranch} to ${validated.target_branch} and ${validated.cleanup_worktree ? 'cleaned up worktree' : 'kept worktree'}`
461
- }
462
- });
463
- return {
464
- merge_commit_hash: mergeCommitHash,
465
- merged_from: worktreeBranch,
466
- merged_to: validated.target_branch,
467
- cleanup_result: cleanupResult,
468
- current_branch: validated.target_branch
469
- };
470
- }
471
- else {
472
- // FIXED: Now that we have proper worktree creation, this should not happen
473
- throw new Error(`No worktree found for SPEC ${validated.spec_id}. Please ensure the worktree was created properly with workflow.start.`);
474
- }
475
- }
476
- catch (error) {
477
- throw new Error(`Failed to auto-merge and cleanup: ${error}`);
478
- }
479
- }
480
- // Smart git operations for completion workflow
481
- export async function completeWorkflowGitOperations(spec_id) {
482
- try {
483
- // Get current git status
484
- const gitStatus = await getGitStatus();
485
- // Auto-commit any remaining changes
486
- if (!gitStatus.is_clean) {
487
- const allChangedFiles = [
488
- ...gitStatus.staged_files,
489
- ...gitStatus.unstaged_files,
490
- ...gitStatus.untracked_files
491
- ];
492
- await autoCommitChanges({
493
- spec_id,
494
- files_changed: allChangedFiles,
495
- action_description: "finalize implementation and prepare for completion"
496
- });
497
- }
498
- // Auto-merge and cleanup
499
- const mergeResult = await autoMergeAndCleanup({
500
- spec_id,
501
- target_branch: "main",
502
- cleanup_worktree: true
503
- });
504
- // Update SPEC with completion status
505
- await specUpdateHandler({
506
- spec_id,
507
- execution_log: {
508
- timestamp: new Date().toISOString(),
509
- engineer: "BOB_GIT_AUTOMATION",
510
- action: "user_satisfied",
511
- task_id: "WORKFLOW-COMPLETE",
512
- files_changed: [],
513
- commit_hash: mergeResult.merge_commit_hash,
514
- note: "Workflow completed successfully: merged to main and cleaned up worktree"
515
- }
516
- });
517
- return {
518
- workflow_status: "completed",
519
- final_commit: mergeResult.merge_commit_hash,
520
- merged_to: mergeResult.merged_to,
521
- cleanup_successful: !!mergeResult.cleanup_result?.branch_deleted
522
- };
523
- }
524
- catch (error) {
525
- throw new Error(`Failed to complete workflow git operations: ${error}`);
526
- }
527
- }
528
- // ============================================================================
529
- // NEW MCP TOOLS: Streamlined Worktree-Aware Git Operations
530
- // ============================================================================
531
- export const GitCommitInput = z.object({
532
- spec_id: z.string().describe("SPEC ID for context and logging"),
533
- worktree_path: z.string().optional().describe("Optional worktree path (auto-detected from SPEC if not provided)"),
534
- commit_message: z.string().describe("Commit message"),
535
- files: z.array(z.string()).optional().describe("Specific files to commit (stages all changes if not provided)")
536
- });
537
- export const GitCommitOutput = z.object({
538
- commit_hash: z.string(),
539
- committed_files: z.array(z.string()),
540
- worktree_path: z.string().optional(),
541
- message: z.string()
542
- });
543
- export const GitMergeInput = z.object({
544
- spec_id: z.string().describe("SPEC ID for the worktree to merge"),
545
- worktree_branch: z.string().optional().describe("Branch to merge (auto-detected from SPEC if not provided)"),
546
- target_branch: z.string().default("main").describe("Target branch to merge into"),
547
- cleanup_worktree: z.boolean().default(true).describe("Remove worktree after successful merge")
548
- });
549
- export const GitMergeOutput = z.object({
550
- merge_commit_hash: z.string(),
551
- merged_from: z.string(),
552
- merged_to: z.string(),
553
- worktree_cleaned: z.boolean(),
554
- conflicts: z.array(z.string()).optional()
555
- });
556
- /**
557
- * bob.git.commit - Worktree-aware commit handler
558
- * Commits changes in the correct worktree context for a SPEC
559
- */
560
- export async function gitCommitHandler(input) {
561
- const validated = GitCommitInput.parse(input);
562
- try {
563
- console.log(`bob.git.commit called for SPEC: ${validated.spec_id}`);
564
- // Find worktree path if not provided
565
- let worktreePath = validated.worktree_path;
566
- if (!worktreePath) {
567
- worktreePath = await findWorktreeForSpec(validated.spec_id) || undefined;
568
- }
569
- // Determine working directory (worktree or main repo)
570
- const workingDir = worktreePath || process.cwd();
571
- console.log(`Committing in: ${workingDir}`);
572
- // Get git status in the target directory
573
- const { stdout: statusOutput } = await execAsyncInDir('git status --porcelain', workingDir);
574
- const hasChanges = statusOutput.trim().length > 0;
575
- if (!hasChanges) {
576
- const { stdout: hashOutput } = await execAsyncInDir('git rev-parse HEAD', workingDir);
577
- const commitHash = hashOutput.trim().substring(0, 8);
578
- return {
579
- commit_hash: commitHash,
580
- committed_files: [],
581
- worktree_path: worktreePath,
582
- message: 'No changes to commit - working tree clean'
583
- };
584
- }
585
- // Stage files
586
- if (validated.files && validated.files.length > 0) {
587
- for (const file of validated.files) {
588
- await execAsyncInDir(`git add "${file}"`, workingDir);
589
- }
590
- }
591
- else {
592
- await execAsyncInDir('git add -A', workingDir);
593
- }
594
- // Create commit using -F to read message from file (heredoc doesn't work with execAsync)
595
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bob-commit-'));
596
- const tmpFile = path.join(tmpDir, 'commit-msg.txt');
597
- fs.writeFileSync(tmpFile, validated.commit_message, 'utf8');
598
- let commitOutput;
599
- try {
600
- const result = await execAsyncInDir(`git commit -F "${tmpFile}"`, workingDir);
601
- commitOutput = result.stdout;
602
- }
603
- finally {
604
- // Clean up temp file
605
- fs.rmSync(tmpDir, { recursive: true, force: true });
606
- }
607
- // Get commit hash
608
- const { stdout: hashOutput } = await execAsyncInDir('git rev-parse HEAD', workingDir);
609
- const commitHash = hashOutput.trim().substring(0, 8);
610
- // Get list of committed files
611
- const { stdout: diffOutput } = await execAsyncInDir(`git diff-tree --no-commit-id --name-only -r ${commitHash}`, workingDir);
612
- const committedFiles = diffOutput.trim().split('\n').filter(f => f.length > 0);
613
- // Log to SPEC
614
- await specUpdateHandler({
615
- spec_id: validated.spec_id,
616
- execution_log: {
617
- timestamp: new Date().toISOString(),
618
- engineer: "bob.git.commit",
619
- action: "commit",
620
- task_id: "GIT-COMMIT",
621
- files_changed: committedFiles,
622
- commit_hash: commitHash,
623
- note: `Committed ${committedFiles.length} files: ${validated.commit_message.split('\n')[0]}`
624
- }
625
- });
626
- return {
627
- commit_hash: commitHash,
628
- committed_files: committedFiles,
629
- worktree_path: worktreePath,
630
- message: commitOutput.trim()
631
- };
632
- }
633
- catch (error) {
634
- throw new Error(`bob.git.commit failed: ${error instanceof Error ? error.message : String(error)}`);
635
- }
636
- }
637
- /**
638
- * bob.git.merge - Worktree → main merge handler
639
- * Handles the complete merge workflow with conflict detection
640
- */
641
- export async function gitMergeHandler(input) {
642
- const validated = GitMergeInput.parse(input);
643
- try {
644
- console.log(`bob.git.merge called for SPEC: ${validated.spec_id}`);
645
- // Get SPEC info
646
- const spec = await specGetHandler({ spec_id: validated.spec_id });
647
- // Determine branch to merge
648
- let branchToMerge = validated.worktree_branch;
649
- if (!branchToMerge) {
650
- // Try to get from SPEC metadata
651
- if (spec.worktree && spec.worktree.branch) {
652
- branchToMerge = spec.worktree.branch;
653
- }
654
- else {
655
- throw new Error(`Cannot determine branch to merge. Provide worktree_branch or ensure SPEC has worktree metadata.`);
656
- }
657
- }
658
- console.log(`Merging branch: ${branchToMerge} → ${validated.target_branch}`);
659
- // Ensure we're on the target branch in main repo
660
- const { stdout: currentBranchOutput } = await execAsync('git branch --show-current');
661
- const currentBranch = currentBranchOutput.trim();
662
- if (currentBranch !== validated.target_branch) {
663
- console.log(`Switching from ${currentBranch} to ${validated.target_branch}`);
664
- await execAsync(`git checkout ${validated.target_branch}`);
665
- }
666
- // Pull latest changes
667
- try {
668
- await execAsync(`git pull origin ${validated.target_branch}`);
669
- }
670
- catch (pullError) {
671
- console.warn(`Could not pull latest ${validated.target_branch}:`, pullError);
672
- }
673
- // Check for merge conflicts (dry-run)
674
- try {
675
- await execAsync(`git merge --no-commit --no-ff ${branchToMerge}`);
676
- await execAsync('git merge --abort'); // Abort dry-run
677
- }
678
- catch (dryRunError) {
679
- // Check if it's a conflict
680
- const { stdout: statusOutput } = await execAsync('git status --porcelain');
681
- if (statusOutput.includes('UU ')) {
682
- const conflicts = statusOutput.split('\n')
683
- .filter(line => line.startsWith('UU '))
684
- .map(line => line.substring(3));
685
- await execAsync('git merge --abort'); // Clean up
686
- return {
687
- merge_commit_hash: '',
688
- merged_from: branchToMerge,
689
- merged_to: validated.target_branch,
690
- worktree_cleaned: false,
691
- conflicts
692
- };
693
- }
694
- }
695
- // Perform actual merge
696
- const { stdout: mergeOutput } = await execAsync(`git merge --no-ff ${branchToMerge} -m "Merge ${branchToMerge} into ${validated.target_branch}"`);
697
- // Get merge commit hash
698
- const { stdout: hashOutput } = await execAsync('git rev-parse HEAD');
699
- const mergeCommitHash = hashOutput.trim().substring(0, 8);
700
- // Cleanup worktree if requested
701
- let worktreeCleaned = false;
702
- if (validated.cleanup_worktree && branchToMerge) {
703
- try {
704
- const { worktreeRemoveHandler } = await import("./worktreeTools.js");
705
- await worktreeRemoveHandler({ worktree_id: branchToMerge, spec_id: validated.spec_id });
706
- worktreeCleaned = true;
707
- }
708
- catch (cleanupError) {
709
- console.warn(`Worktree cleanup failed (non-critical):`, cleanupError);
710
- }
711
- }
712
- // Update SPEC
713
- await specUpdateHandler({
714
- spec_id: validated.spec_id,
715
- execution_log: {
716
- timestamp: new Date().toISOString(),
717
- engineer: "bob.git.merge",
718
- action: "merge",
719
- task_id: "GIT-MERGE",
720
- files_changed: [],
721
- commit_hash: mergeCommitHash,
722
- note: `Merged ${branchToMerge} → ${validated.target_branch}. Worktree cleanup: ${worktreeCleaned}`
723
- },
724
- worktree: worktreeCleaned ? {
725
- status: "merged",
726
- removed_at: new Date().toISOString()
727
- } : undefined
728
- });
729
- return {
730
- merge_commit_hash: mergeCommitHash,
731
- merged_from: branchToMerge,
732
- merged_to: validated.target_branch,
733
- worktree_cleaned: worktreeCleaned,
734
- conflicts: undefined
735
- };
736
- }
737
- catch (error) {
738
- throw new Error(`bob.git.merge failed: ${error instanceof Error ? error.message : String(error)}`);
739
- }
740
- }
741
- //# sourceMappingURL=gitTools.js.map