bobs-workshop 0.1.4
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/LICENSE +21 -0
- package/README.md +252 -0
- package/bin/bobs-mcp.js +130 -0
- package/dist/api/taskLogger.js +106 -0
- package/dist/api/taskLogger.js.map +1 -0
- package/dist/cli/checker.js +401 -0
- package/dist/cli/checker.js.map +1 -0
- package/dist/cli/cleanup.js +131 -0
- package/dist/cli/cleanup.js.map +1 -0
- package/dist/cli/debug.js +157 -0
- package/dist/cli/debug.js.map +1 -0
- package/dist/cli/health.js +97 -0
- package/dist/cli/health.js.map +1 -0
- package/dist/cli/setup.js +81 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/workshop.js +42 -0
- package/dist/cli/workshop.js.map +1 -0
- package/dist/dashboard/server.js +1206 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.js +757 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/architect.js +157 -0
- package/dist/prompts/architect.js.map +1 -0
- package/dist/prompts/debugger.js +201 -0
- package/dist/prompts/debugger.js.map +1 -0
- package/dist/prompts/engineer.js +171 -0
- package/dist/prompts/engineer.js.map +1 -0
- package/dist/prompts/orchestrator.js +225 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/prompts/reviewer.js +199 -0
- package/dist/prompts/reviewer.js.map +1 -0
- package/dist/services/activitySummarizer.js +353 -0
- package/dist/services/activitySummarizer.js.map +1 -0
- package/dist/services/changeValidator.js +396 -0
- package/dist/services/changeValidator.js.map +1 -0
- package/dist/services/claudeOrchestrator.js +343 -0
- package/dist/services/claudeOrchestrator.js.map +1 -0
- package/dist/services/fileMonitor.js +250 -0
- package/dist/services/fileMonitor.js.map +1 -0
- package/dist/services/implementationSummarizer.js +306 -0
- package/dist/services/implementationSummarizer.js.map +1 -0
- package/dist/services/liveMonitor.js +315 -0
- package/dist/services/liveMonitor.js.map +1 -0
- package/dist/services/mcpAuditLogger.js +104 -0
- package/dist/services/mcpAuditLogger.js.map +1 -0
- package/dist/services/mcpLogger.js +223 -0
- package/dist/services/mcpLogger.js.map +1 -0
- package/dist/services/tmuxManager.js +541 -0
- package/dist/services/tmuxManager.js.map +1 -0
- package/dist/tools/approvalTools.js +244 -0
- package/dist/tools/approvalTools.js.map +1 -0
- package/dist/tools/autoDebugger.js +147 -0
- package/dist/tools/autoDebugger.js.map +1 -0
- package/dist/tools/cleanupService.js +221 -0
- package/dist/tools/cleanupService.js.map +1 -0
- package/dist/tools/dashboardTools.js +359 -0
- package/dist/tools/dashboardTools.js.map +1 -0
- package/dist/tools/developmentNudges.js +336 -0
- package/dist/tools/developmentNudges.js.map +1 -0
- package/dist/tools/gitTools.js +741 -0
- package/dist/tools/gitTools.js.map +1 -0
- package/dist/tools/orchestratorTools.js +765 -0
- package/dist/tools/orchestratorTools.js.map +1 -0
- package/dist/tools/searchTools.js +788 -0
- package/dist/tools/searchTools.js.map +1 -0
- package/dist/tools/specTools.js +350 -0
- package/dist/tools/specTools.js.map +1 -0
- package/dist/tools/tmuxTools.js +100 -0
- package/dist/tools/tmuxTools.js.map +1 -0
- package/dist/tools/workRecorder.js +215 -0
- package/dist/tools/workRecorder.js.map +1 -0
- package/dist/tools/worktreeTools.js +705 -0
- package/dist/tools/worktreeTools.js.map +1 -0
- package/dist/utils/__tests__/integration.test.js +57 -0
- package/dist/utils/__tests__/integration.test.js.map +1 -0
- package/dist/utils/__tests__/serverDetection.test.js +151 -0
- package/dist/utils/__tests__/serverDetection.test.js.map +1 -0
- package/dist/utils/errorHandling.js +336 -0
- package/dist/utils/errorHandling.js.map +1 -0
- package/dist/utils/processManager.js +172 -0
- package/dist/utils/processManager.js.map +1 -0
- package/dist/utils/reliability.js +263 -0
- package/dist/utils/reliability.js.map +1 -0
- package/dist/utils/responseFormatter.js +250 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/utils/serverDetection.js +133 -0
- package/dist/utils/serverDetection.js.map +1 -0
- package/dist/utils/specMigration.js +105 -0
- package/dist/utils/specMigration.js.map +1 -0
- package/dist/validation/schemas.js +299 -0
- package/dist/validation/schemas.js.map +1 -0
- package/package.json +79 -0
- package/scripts/init-workspace.js +63 -0
- package/scripts/install-search-tools.js +116 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
// src/tools/worktreeTools.ts
|
|
2
|
+
import simpleGit from "simple-git";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import path from "path";
|
|
6
|
+
const git = simpleGit();
|
|
7
|
+
export const WorktreeCreateInput = z.object({
|
|
8
|
+
spec_id: z.string(),
|
|
9
|
+
branch: z.string(),
|
|
10
|
+
base: z.string().optional().default("main")
|
|
11
|
+
});
|
|
12
|
+
export const WorktreeCreateOutput = z.object({
|
|
13
|
+
worktree_id: z.string(),
|
|
14
|
+
branch: z.string(),
|
|
15
|
+
path: z.string(),
|
|
16
|
+
status: z.string()
|
|
17
|
+
});
|
|
18
|
+
export const WorktreeStatusInput = z.object({
|
|
19
|
+
worktree_id: z.string()
|
|
20
|
+
});
|
|
21
|
+
export const WorktreeStatusOutput = z.object({
|
|
22
|
+
branch: z.string(),
|
|
23
|
+
dirty: z.boolean(),
|
|
24
|
+
ahead: z.number(),
|
|
25
|
+
behind: z.number(),
|
|
26
|
+
changed_files: z.array(z.string())
|
|
27
|
+
});
|
|
28
|
+
export const WorktreeMergeInput = z.object({
|
|
29
|
+
worktree_id: z.string()
|
|
30
|
+
});
|
|
31
|
+
export const WorktreeRemoveInput = z.object({
|
|
32
|
+
worktree_id: z.string(),
|
|
33
|
+
spec_id: z.string().optional()
|
|
34
|
+
});
|
|
35
|
+
export const WorktreeListOutput = z.object({
|
|
36
|
+
committed: z.array(z.string()),
|
|
37
|
+
dirty: z.array(z.string()),
|
|
38
|
+
clean: z.array(z.string())
|
|
39
|
+
});
|
|
40
|
+
export const WorktreeStartServerInput = z.object({
|
|
41
|
+
worktree_id: z.string(),
|
|
42
|
+
command: z.string().default("npm run dev"),
|
|
43
|
+
port: z.number().optional()
|
|
44
|
+
});
|
|
45
|
+
export const WorktreeStartServerOutput = z.object({
|
|
46
|
+
status: z.string(),
|
|
47
|
+
url: z.string().optional(),
|
|
48
|
+
pid: z.number()
|
|
49
|
+
});
|
|
50
|
+
export const WorktreeStopServerInput = z.object({
|
|
51
|
+
pid: z.number()
|
|
52
|
+
});
|
|
53
|
+
export const WorktreeStopServerOutput = z.object({
|
|
54
|
+
status: z.string()
|
|
55
|
+
});
|
|
56
|
+
// Store running server processes
|
|
57
|
+
const runningServers = new Map();
|
|
58
|
+
export async function worktreeCreateHandler(input) {
|
|
59
|
+
const validated = WorktreeCreateInput.parse(input);
|
|
60
|
+
const branchName = validated.branch || `feature/${validated.spec_id}`;
|
|
61
|
+
try {
|
|
62
|
+
console.log(`🔧 Creating actual git worktree for branch: ${branchName}`);
|
|
63
|
+
// Create the worktree directory path
|
|
64
|
+
const worktreePath = path.resolve(process.cwd(), `.bob/worktrees/${branchName}`);
|
|
65
|
+
// Ensure the worktrees directory exists
|
|
66
|
+
const fs = await import('fs-extra');
|
|
67
|
+
await fs.ensureDir(path.dirname(worktreePath));
|
|
68
|
+
// FIXED: Actually create a git worktree (not just a branch)
|
|
69
|
+
const { exec } = await import('child_process');
|
|
70
|
+
const { promisify } = await import('util');
|
|
71
|
+
const execAsync = promisify(exec);
|
|
72
|
+
// Create the worktree with a new branch
|
|
73
|
+
console.log(`Creating worktree at: ${worktreePath}`);
|
|
74
|
+
await execAsync(`git worktree add ${worktreePath} -b ${branchName} ${validated.base}`);
|
|
75
|
+
// Install dependencies in the new worktree
|
|
76
|
+
console.log(`Installing dependencies in worktree: ${worktreePath}`);
|
|
77
|
+
await installDependenciesInWorktree(worktreePath);
|
|
78
|
+
// Update SPEC with worktree creation and metadata
|
|
79
|
+
if (validated.spec_id) {
|
|
80
|
+
const { specUpdateHandler } = await import("./specTools.js");
|
|
81
|
+
await specUpdateHandler({
|
|
82
|
+
spec_id: validated.spec_id,
|
|
83
|
+
worktree: {
|
|
84
|
+
branch: branchName,
|
|
85
|
+
path: worktreePath,
|
|
86
|
+
status: "active",
|
|
87
|
+
created_at: new Date().toISOString()
|
|
88
|
+
},
|
|
89
|
+
execution_log: {
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
engineer: "system",
|
|
92
|
+
action: "worktree_created",
|
|
93
|
+
task_id: "system-worktree-creation",
|
|
94
|
+
files_changed: [],
|
|
95
|
+
commit_hash: "pending",
|
|
96
|
+
note: `Created actual git worktree at ${worktreePath} with branch ${branchName} and installed dependencies`
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
console.log(`✅ Worktree created successfully at: ${worktreePath}`);
|
|
101
|
+
return {
|
|
102
|
+
worktree_id: branchName,
|
|
103
|
+
branch: branchName,
|
|
104
|
+
path: worktreePath,
|
|
105
|
+
status: "created"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Helper function to install dependencies in a worktree
|
|
113
|
+
async function installDependenciesInWorktree(worktreePath) {
|
|
114
|
+
try {
|
|
115
|
+
const { exec } = await import('child_process');
|
|
116
|
+
const { promisify } = await import('util');
|
|
117
|
+
const execAsync = promisify(exec);
|
|
118
|
+
const fs = await import('fs-extra');
|
|
119
|
+
// Check if package.json exists in the worktree
|
|
120
|
+
const packageJsonPath = path.join(worktreePath, 'package.json');
|
|
121
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
122
|
+
console.log(`Installing npm dependencies in ${worktreePath}...`);
|
|
123
|
+
// Install dependencies (use npm ci for faster, reliable installs)
|
|
124
|
+
try {
|
|
125
|
+
await execAsync('npm ci', { cwd: worktreePath });
|
|
126
|
+
console.log(`✅ Dependencies installed successfully in ${worktreePath}`);
|
|
127
|
+
}
|
|
128
|
+
catch (ciError) {
|
|
129
|
+
// Fallback to npm install if npm ci fails
|
|
130
|
+
console.log(`npm ci failed, falling back to npm install...`);
|
|
131
|
+
await execAsync('npm install', { cwd: worktreePath });
|
|
132
|
+
console.log(`✅ Dependencies installed successfully in ${worktreePath}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.log(`No package.json found in ${worktreePath}, skipping dependency installation`);
|
|
137
|
+
}
|
|
138
|
+
// Check for other package managers
|
|
139
|
+
const yarnLockPath = path.join(worktreePath, 'yarn.lock');
|
|
140
|
+
const pnpmLockPath = path.join(worktreePath, 'pnpm-lock.yaml');
|
|
141
|
+
if (await fs.pathExists(yarnLockPath)) {
|
|
142
|
+
console.log(`Installing yarn dependencies in ${worktreePath}...`);
|
|
143
|
+
await execAsync('yarn install --frozen-lockfile', { cwd: worktreePath });
|
|
144
|
+
console.log(`✅ Yarn dependencies installed successfully in ${worktreePath}`);
|
|
145
|
+
}
|
|
146
|
+
else if (await fs.pathExists(pnpmLockPath)) {
|
|
147
|
+
console.log(`Installing pnpm dependencies in ${worktreePath}...`);
|
|
148
|
+
await execAsync('pnpm install --frozen-lockfile', { cwd: worktreePath });
|
|
149
|
+
console.log(`✅ PNPM dependencies installed successfully in ${worktreePath}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.warn(`Warning: Failed to install dependencies in worktree: ${error}`);
|
|
154
|
+
// Don't throw - dependency installation failure shouldn't break worktree creation
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function worktreeStatusHandler(input) {
|
|
158
|
+
const validated = WorktreeStatusInput.parse(input);
|
|
159
|
+
try {
|
|
160
|
+
const status = await git.status();
|
|
161
|
+
return {
|
|
162
|
+
branch: validated.worktree_id,
|
|
163
|
+
dirty: !status.isClean(),
|
|
164
|
+
ahead: 0, // simplified for now
|
|
165
|
+
behind: 0, // simplified for now
|
|
166
|
+
changed_files: [...status.modified, ...status.created, ...status.deleted]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
throw new Error(`Failed to get worktree status: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function worktreeMergeHandler(input) {
|
|
174
|
+
const validated = WorktreeMergeInput.parse(input);
|
|
175
|
+
try {
|
|
176
|
+
// Safety checks before merging
|
|
177
|
+
const currentBranch = await git.branch();
|
|
178
|
+
const status = await git.status();
|
|
179
|
+
// Ensure we're not on the branch being merged
|
|
180
|
+
if (currentBranch.current === validated.worktree_id) {
|
|
181
|
+
throw new Error(`Cannot merge while on branch ${validated.worktree_id}. Switch to main first.`);
|
|
182
|
+
}
|
|
183
|
+
// Check if main branch is clean
|
|
184
|
+
if (!status.isClean()) {
|
|
185
|
+
throw new Error("Main branch has uncommitted changes. Please commit or stash them before merging.");
|
|
186
|
+
}
|
|
187
|
+
// Check if branch exists
|
|
188
|
+
const branches = Object.keys(currentBranch.branches);
|
|
189
|
+
if (!branches.includes(validated.worktree_id)) {
|
|
190
|
+
throw new Error(`Branch ${validated.worktree_id} does not exist`);
|
|
191
|
+
}
|
|
192
|
+
// Switch to main and pull latest (if remote exists)
|
|
193
|
+
await git.checkout("main");
|
|
194
|
+
try {
|
|
195
|
+
// Try to pull latest main (ignore errors if no remote)
|
|
196
|
+
await git.pull("origin", "main");
|
|
197
|
+
}
|
|
198
|
+
catch (pullError) {
|
|
199
|
+
console.warn("Could not pull latest main (no remote or network issue):", pullError);
|
|
200
|
+
}
|
|
201
|
+
// Check for merge conflicts by doing a dry run
|
|
202
|
+
try {
|
|
203
|
+
await git.raw(['merge', '--no-commit', '--no-ff', validated.worktree_id]);
|
|
204
|
+
// If successful, abort the merge to complete it properly
|
|
205
|
+
await git.merge(['--abort']);
|
|
206
|
+
}
|
|
207
|
+
catch (dryRunError) {
|
|
208
|
+
throw new Error(`Merge would result in conflicts: ${dryRunError instanceof Error ? dryRunError.message : String(dryRunError)}`);
|
|
209
|
+
}
|
|
210
|
+
// Perform the actual merge
|
|
211
|
+
await git.merge([validated.worktree_id, '--no-ff']);
|
|
212
|
+
return {
|
|
213
|
+
status: "merged",
|
|
214
|
+
commit_hash: await git.revparse('HEAD'),
|
|
215
|
+
note: `Successfully merged ${validated.worktree_id} into main`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Ensure we're back on main if merge failed
|
|
220
|
+
try {
|
|
221
|
+
const currentBranch = await git.branch();
|
|
222
|
+
if (currentBranch.current !== "main") {
|
|
223
|
+
await git.checkout("main");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (cleanupError) {
|
|
227
|
+
console.warn("Failed to cleanup after merge error:", cleanupError);
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Failed to merge worktree: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function worktreeRemoveHandler(input) {
|
|
233
|
+
const validated = WorktreeRemoveInput.parse(input);
|
|
234
|
+
try {
|
|
235
|
+
console.log(`🔧 Removing worktree: ${validated.worktree_id}`);
|
|
236
|
+
// FIXED: Actually remove the git worktree, not just the branch
|
|
237
|
+
const { exec } = await import('child_process');
|
|
238
|
+
const { promisify } = await import('util');
|
|
239
|
+
const execAsync = promisify(exec);
|
|
240
|
+
const fs = await import('fs-extra');
|
|
241
|
+
const worktreePath = path.resolve(process.cwd(), `.bob/worktrees/${validated.worktree_id}`);
|
|
242
|
+
// Check if worktree exists
|
|
243
|
+
if (await fs.pathExists(worktreePath)) {
|
|
244
|
+
console.log(`Removing worktree directory: ${worktreePath}`);
|
|
245
|
+
// Remove the worktree using git command
|
|
246
|
+
await execAsync(`git worktree remove ${worktreePath} --force`);
|
|
247
|
+
console.log(`✅ Worktree removed: ${worktreePath}`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.log(`Worktree directory not found: ${worktreePath}`);
|
|
251
|
+
}
|
|
252
|
+
// Switch to main branch in main repository
|
|
253
|
+
await git.checkout("main");
|
|
254
|
+
// Delete the local branch
|
|
255
|
+
try {
|
|
256
|
+
await git.deleteLocalBranch(validated.worktree_id);
|
|
257
|
+
console.log(`✅ Branch deleted: ${validated.worktree_id}`);
|
|
258
|
+
}
|
|
259
|
+
catch (branchError) {
|
|
260
|
+
console.log(`Warning: Could not delete branch ${validated.worktree_id}:`, branchError);
|
|
261
|
+
// Don't fail the whole operation if branch deletion fails
|
|
262
|
+
}
|
|
263
|
+
// Update SPEC with worktree removal
|
|
264
|
+
if (validated.spec_id) {
|
|
265
|
+
try {
|
|
266
|
+
const { specUpdateHandler } = await import("./specTools.js");
|
|
267
|
+
await specUpdateHandler({
|
|
268
|
+
spec_id: validated.spec_id,
|
|
269
|
+
worktree: {
|
|
270
|
+
status: "removed",
|
|
271
|
+
removed_at: new Date().toISOString()
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (specError) {
|
|
276
|
+
console.warn(`Warning: Could not update SPEC ${validated.spec_id}:`, specError);
|
|
277
|
+
// Don't fail the whole operation if SPEC update fails
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
status: "removed"
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
throw new Error(`Failed to remove worktree: ${error instanceof Error ? error.message : String(error)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export async function worktreeListHandler() {
|
|
289
|
+
try {
|
|
290
|
+
const branchInfo = await git.branch();
|
|
291
|
+
const status = await git.status();
|
|
292
|
+
// Filter out remote branches and main branch - only show local feature branches
|
|
293
|
+
const branches = Object.keys(branchInfo.branches).filter(name => name !== "main" &&
|
|
294
|
+
!name.startsWith("remotes/") &&
|
|
295
|
+
!name.includes("origin/"));
|
|
296
|
+
const committed = [];
|
|
297
|
+
const dirty = [];
|
|
298
|
+
const clean = [];
|
|
299
|
+
for (const branch of branches) {
|
|
300
|
+
const branchData = {
|
|
301
|
+
name: branch
|
|
302
|
+
};
|
|
303
|
+
if (branch === branchInfo.current && !status.isClean()) {
|
|
304
|
+
dirty.push(branchData);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
// simplified branch categorization for now
|
|
308
|
+
clean.push(branchData);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
committed,
|
|
313
|
+
dirty,
|
|
314
|
+
clean
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw new Error(`Failed to list worktrees: ${error instanceof Error ? error.message : String(error)}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
export async function worktreeStartServerHandler(input) {
|
|
322
|
+
const validated = WorktreeStartServerInput.parse(input);
|
|
323
|
+
try {
|
|
324
|
+
// FIXED: Find the actual worktree directory instead of just switching branches
|
|
325
|
+
const worktreePath = path.resolve(process.cwd(), `.bob/worktrees/${validated.worktree_id}`);
|
|
326
|
+
// Check if worktree exists
|
|
327
|
+
const fs = await import('fs-extra');
|
|
328
|
+
if (!await fs.pathExists(worktreePath)) {
|
|
329
|
+
throw new Error(`Worktree not found at ${worktreePath}. Please create the worktree first.`);
|
|
330
|
+
}
|
|
331
|
+
console.log(`Starting server in worktree: ${worktreePath}`);
|
|
332
|
+
// Parse command parts
|
|
333
|
+
const commandParts = validated.command.split(" ");
|
|
334
|
+
const command = commandParts[0];
|
|
335
|
+
const args = commandParts.slice(1);
|
|
336
|
+
// Spawn the server process in the worktree directory
|
|
337
|
+
const serverProcess = spawn(command, args, {
|
|
338
|
+
cwd: worktreePath, // FIXED: Run in the actual worktree directory
|
|
339
|
+
stdio: 'pipe',
|
|
340
|
+
detached: false,
|
|
341
|
+
env: { ...process.env, PORT: validated.port?.toString() || process.env.PORT }
|
|
342
|
+
});
|
|
343
|
+
if (!serverProcess.pid) {
|
|
344
|
+
throw new Error("Failed to start server process");
|
|
345
|
+
}
|
|
346
|
+
// Store the process for later management
|
|
347
|
+
runningServers.set(serverProcess.pid, serverProcess);
|
|
348
|
+
// Log server output for debugging
|
|
349
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
350
|
+
console.log(`[SERVER ${serverProcess.pid}] ${data.toString()}`);
|
|
351
|
+
});
|
|
352
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
353
|
+
console.log(`[SERVER ${serverProcess.pid}] ERROR: ${data.toString()}`);
|
|
354
|
+
});
|
|
355
|
+
// Determine URL if port is known
|
|
356
|
+
let url;
|
|
357
|
+
if (validated.port) {
|
|
358
|
+
url = `http://localhost:${validated.port}`;
|
|
359
|
+
}
|
|
360
|
+
else if (validated.command.includes("dev") || validated.command.includes("start")) {
|
|
361
|
+
// Common development server ports
|
|
362
|
+
url = "http://localhost:3000";
|
|
363
|
+
}
|
|
364
|
+
// Update SPEC with server start event
|
|
365
|
+
const { specUpdateHandler } = await import("./specTools.js");
|
|
366
|
+
await specUpdateHandler({
|
|
367
|
+
spec_id: validated.worktree_id.replace("feature/", ""),
|
|
368
|
+
execution_log: {
|
|
369
|
+
timestamp: new Date().toISOString(),
|
|
370
|
+
engineer: "system",
|
|
371
|
+
action: "server_started",
|
|
372
|
+
task_id: "system-server-start",
|
|
373
|
+
files_changed: [],
|
|
374
|
+
commit_hash: "pending",
|
|
375
|
+
note: `Started server in worktree ${worktreePath}: ${validated.command} (PID: ${serverProcess.pid})`
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
console.log(`✅ Server started successfully in worktree (PID: ${serverProcess.pid})`);
|
|
379
|
+
return {
|
|
380
|
+
status: "running",
|
|
381
|
+
url,
|
|
382
|
+
pid: serverProcess.pid
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
throw new Error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
export async function worktreeStopServerHandler(input) {
|
|
390
|
+
const validated = WorktreeStopServerInput.parse(input);
|
|
391
|
+
try {
|
|
392
|
+
const serverProcess = runningServers.get(validated.pid);
|
|
393
|
+
if (!serverProcess) {
|
|
394
|
+
throw new Error(`No running server found with PID ${validated.pid}`);
|
|
395
|
+
}
|
|
396
|
+
// Kill the process
|
|
397
|
+
serverProcess.kill('SIGTERM');
|
|
398
|
+
// Remove from tracking
|
|
399
|
+
runningServers.delete(validated.pid);
|
|
400
|
+
return {
|
|
401
|
+
status: "stopped"
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new Error(`Failed to stop server: ${error instanceof Error ? error.message : String(error)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// NEW MCP TOOL: Comprehensive Worktree Debug & Maintenance
|
|
410
|
+
// ============================================================================
|
|
411
|
+
export const WorktreeDebugInput = z.object({
|
|
412
|
+
action: z.enum(["repair", "cleanup", "validate", "status"]).describe("Debug action to perform"),
|
|
413
|
+
spec_id: z.string().optional().describe("Optional SPEC ID to focus on"),
|
|
414
|
+
max_age_days: z.number().optional().default(7).describe("Max age in days for cleanup action")
|
|
415
|
+
});
|
|
416
|
+
export const WorktreeDebugOutput = z.object({
|
|
417
|
+
action: z.string(),
|
|
418
|
+
success: z.boolean(),
|
|
419
|
+
report: z.string(),
|
|
420
|
+
actions_taken: z.array(z.string()),
|
|
421
|
+
recommendations: z.array(z.string()).optional(),
|
|
422
|
+
worktrees_found: z.number().optional(),
|
|
423
|
+
worktrees_cleaned: z.number().optional(),
|
|
424
|
+
issues_found: z.array(z.string()).optional()
|
|
425
|
+
});
|
|
426
|
+
/**
|
|
427
|
+
* bob.worktree.debug - Comprehensive worktree maintenance tool
|
|
428
|
+
* Handles repair, cleanup, validation, and status reporting
|
|
429
|
+
*/
|
|
430
|
+
export async function worktreeDebugHandler(input) {
|
|
431
|
+
const validated = WorktreeDebugInput.parse(input);
|
|
432
|
+
try {
|
|
433
|
+
console.log(`bob.worktree.debug called with action: ${validated.action}`);
|
|
434
|
+
switch (validated.action) {
|
|
435
|
+
case "status":
|
|
436
|
+
return await debugStatus(validated.spec_id);
|
|
437
|
+
case "validate":
|
|
438
|
+
return await debugValidate(validated.spec_id);
|
|
439
|
+
case "repair":
|
|
440
|
+
return await debugRepair(validated.spec_id);
|
|
441
|
+
case "cleanup":
|
|
442
|
+
return await debugCleanup(validated.max_age_days);
|
|
443
|
+
default:
|
|
444
|
+
throw new Error(`Unknown debug action: ${validated.action}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
action: validated.action,
|
|
450
|
+
success: false,
|
|
451
|
+
report: `Debug action failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
452
|
+
actions_taken: [],
|
|
453
|
+
recommendations: ["Check logs for detailed error information"],
|
|
454
|
+
issues_found: [String(error)]
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Helper: Get detailed status of all worktrees
|
|
459
|
+
async function debugStatus(specId) {
|
|
460
|
+
const { exec } = await import('child_process');
|
|
461
|
+
const { promisify } = await import('util');
|
|
462
|
+
const execAsync = promisify(exec);
|
|
463
|
+
const fs = await import('fs-extra');
|
|
464
|
+
const actionsTaken = [];
|
|
465
|
+
const issuesFound = [];
|
|
466
|
+
// Get git worktrees
|
|
467
|
+
const { stdout } = await execAsync('git worktree list --porcelain');
|
|
468
|
+
const worktrees = stdout.trim().split('\n\n').filter(w => w.length > 0);
|
|
469
|
+
const worktreeCount = worktrees.length - 1; // Exclude main repo
|
|
470
|
+
actionsTaken.push(`Found ${worktreeCount} worktrees (excluding main repo)`);
|
|
471
|
+
// Parse worktrees
|
|
472
|
+
for (const worktreeInfo of worktrees) {
|
|
473
|
+
const lines = worktreeInfo.split('\n');
|
|
474
|
+
const worktreePath = lines[0].replace('worktree ', '');
|
|
475
|
+
if (worktreePath === process.cwd())
|
|
476
|
+
continue; // Skip main
|
|
477
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
478
|
+
const branch = branchLine ? branchLine.replace('branch refs/heads/', '') : 'unknown';
|
|
479
|
+
// Check if directory exists
|
|
480
|
+
const exists = await fs.pathExists(worktreePath);
|
|
481
|
+
if (!exists) {
|
|
482
|
+
issuesFound.push(`Worktree registered but directory missing: ${worktreePath} (${branch})`);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
actionsTaken.push(`✓ ${branch} → ${worktreePath}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Check SPEC metadata if specId provided
|
|
489
|
+
if (specId) {
|
|
490
|
+
const { specGetHandler } = await import("./specTools.js");
|
|
491
|
+
try {
|
|
492
|
+
const spec = await specGetHandler({ spec_id: specId });
|
|
493
|
+
if (spec.worktree) {
|
|
494
|
+
actionsTaken.push(`SPEC ${specId} worktree metadata: ${spec.worktree.status} | ${spec.worktree.branch} → ${spec.worktree.path}`);
|
|
495
|
+
if (spec.worktree.path && !await fs.pathExists(spec.worktree.path)) {
|
|
496
|
+
issuesFound.push(`SPEC worktree path does not exist: ${spec.worktree.path}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
actionsTaken.push(`SPEC ${specId} has no worktree metadata`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
issuesFound.push(`Could not load SPEC ${specId}: ${error}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
action: "status",
|
|
509
|
+
success: true,
|
|
510
|
+
report: `Status report: ${worktreeCount} worktrees found, ${issuesFound.length} issues detected`,
|
|
511
|
+
actions_taken: actionsTaken,
|
|
512
|
+
recommendations: issuesFound.length > 0 ? ["Run 'repair' action to fix issues"] : undefined,
|
|
513
|
+
worktrees_found: worktreeCount,
|
|
514
|
+
issues_found: issuesFound.length > 0 ? issuesFound : undefined
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
// Helper: Validate worktree exists before operations
|
|
518
|
+
async function debugValidate(specId) {
|
|
519
|
+
if (!specId) {
|
|
520
|
+
return {
|
|
521
|
+
action: "validate",
|
|
522
|
+
success: false,
|
|
523
|
+
report: "Validation requires a spec_id parameter",
|
|
524
|
+
actions_taken: [],
|
|
525
|
+
recommendations: ["Provide spec_id to validate worktree"]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const { specGetHandler } = await import("./specTools.js");
|
|
529
|
+
const fs = await import('fs-extra');
|
|
530
|
+
const actionsTaken = [];
|
|
531
|
+
const issuesFound = [];
|
|
532
|
+
try {
|
|
533
|
+
const spec = await specGetHandler({ spec_id: specId });
|
|
534
|
+
if (!spec.worktree || !spec.worktree.branch || !spec.worktree.path) {
|
|
535
|
+
issuesFound.push("SPEC missing worktree metadata");
|
|
536
|
+
return {
|
|
537
|
+
action: "validate",
|
|
538
|
+
success: false,
|
|
539
|
+
report: "SPEC has incomplete worktree metadata",
|
|
540
|
+
actions_taken: [],
|
|
541
|
+
issues_found: issuesFound,
|
|
542
|
+
recommendations: ["Check if worktree was created properly", "Run workflow.start to create worktree"]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
actionsTaken.push(`SPEC worktree: ${spec.worktree.branch} → ${spec.worktree.path}`);
|
|
546
|
+
actionsTaken.push(`Status: ${spec.worktree.status}`);
|
|
547
|
+
// Validate directory exists
|
|
548
|
+
if (!await fs.pathExists(spec.worktree.path)) {
|
|
549
|
+
issuesFound.push(`Worktree directory does not exist: ${spec.worktree.path}`);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
actionsTaken.push("✓ Worktree directory exists");
|
|
553
|
+
}
|
|
554
|
+
// Validate it's in git worktree list
|
|
555
|
+
const { exec } = await import('child_process');
|
|
556
|
+
const { promisify } = await import('util');
|
|
557
|
+
const execAsync = promisify(exec);
|
|
558
|
+
const { stdout } = await execAsync('git worktree list --porcelain');
|
|
559
|
+
if (!stdout.includes(spec.worktree.path)) {
|
|
560
|
+
issuesFound.push(`Worktree not registered in git: ${spec.worktree.path}`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
actionsTaken.push("✓ Worktree registered in git");
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
action: "validate",
|
|
567
|
+
success: issuesFound.length === 0,
|
|
568
|
+
report: issuesFound.length === 0
|
|
569
|
+
? `Worktree for ${specId} is valid and ready`
|
|
570
|
+
: `Worktree for ${specId} has ${issuesFound.length} issues`,
|
|
571
|
+
actions_taken: actionsTaken,
|
|
572
|
+
issues_found: issuesFound.length > 0 ? issuesFound : undefined,
|
|
573
|
+
recommendations: issuesFound.length > 0 ? ["Run 'repair' action to fix issues"] : undefined
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
return {
|
|
578
|
+
action: "validate",
|
|
579
|
+
success: false,
|
|
580
|
+
report: `Validation failed: ${error}`,
|
|
581
|
+
actions_taken: actionsTaken,
|
|
582
|
+
issues_found: [String(error)]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Helper: Repair worktree inconsistencies
|
|
587
|
+
async function debugRepair(specId) {
|
|
588
|
+
const { exec } = await import('child_process');
|
|
589
|
+
const { promisify } = await import('util');
|
|
590
|
+
const execAsync = promisify(exec);
|
|
591
|
+
const fs = await import('fs-extra');
|
|
592
|
+
const actionsTaken = [];
|
|
593
|
+
const issuesFound = [];
|
|
594
|
+
// Get git worktrees
|
|
595
|
+
const { stdout } = await execAsync('git worktree list --porcelain');
|
|
596
|
+
const worktrees = stdout.trim().split('\n\n');
|
|
597
|
+
// Check for orphaned worktrees (registered but directory missing)
|
|
598
|
+
for (const worktreeInfo of worktrees) {
|
|
599
|
+
const lines = worktreeInfo.split('\n');
|
|
600
|
+
const worktreePath = lines[0].replace('worktree ', '');
|
|
601
|
+
if (worktreePath === process.cwd())
|
|
602
|
+
continue;
|
|
603
|
+
if (!await fs.pathExists(worktreePath)) {
|
|
604
|
+
issuesFound.push(`Orphaned worktree: ${worktreePath}`);
|
|
605
|
+
try {
|
|
606
|
+
await execAsync(`git worktree remove ${worktreePath} --force`);
|
|
607
|
+
actionsTaken.push(`✓ Removed orphaned worktree: ${worktreePath}`);
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
actionsTaken.push(`✗ Failed to remove ${worktreePath}: ${error}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// If specId provided, sync SPEC metadata with reality
|
|
615
|
+
if (specId) {
|
|
616
|
+
const { specGetHandler, specUpdateHandler } = await import("./specTools.js");
|
|
617
|
+
try {
|
|
618
|
+
const spec = await specGetHandler({ spec_id: specId });
|
|
619
|
+
if (spec.worktree && spec.worktree.path) {
|
|
620
|
+
const exists = await fs.pathExists(spec.worktree.path);
|
|
621
|
+
const inGit = stdout.includes(spec.worktree.path);
|
|
622
|
+
if (!exists && !inGit && spec.worktree.status !== "removed") {
|
|
623
|
+
// Worktree is gone but SPEC thinks it's active
|
|
624
|
+
await specUpdateHandler({
|
|
625
|
+
spec_id: specId,
|
|
626
|
+
worktree: {
|
|
627
|
+
status: "removed",
|
|
628
|
+
removed_at: new Date().toISOString()
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
actionsTaken.push(`✓ Updated SPEC ${specId} worktree status to 'removed'`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
actionsTaken.push(`Could not update SPEC ${specId}: ${error}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Prune stale git worktree references
|
|
640
|
+
try {
|
|
641
|
+
await execAsync('git worktree prune');
|
|
642
|
+
actionsTaken.push("✓ Pruned stale git worktree references");
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
actionsTaken.push(`✗ Failed to prune: ${error}`);
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
action: "repair",
|
|
649
|
+
success: true,
|
|
650
|
+
report: `Repair completed: ${actionsTaken.length} actions taken, ${issuesFound.length} issues found`,
|
|
651
|
+
actions_taken: actionsTaken,
|
|
652
|
+
issues_found: issuesFound.length > 0 ? issuesFound : undefined,
|
|
653
|
+
recommendations: issuesFound.length > 0 ? ["Some issues remain - manual intervention may be required"] : undefined
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// Helper: Cleanup stale worktrees
|
|
657
|
+
async function debugCleanup(maxAgeDays) {
|
|
658
|
+
const { exec } = await import('child_process');
|
|
659
|
+
const { promisify } = await import('util');
|
|
660
|
+
const execAsync = promisify(exec);
|
|
661
|
+
const fs = await import('fs-extra');
|
|
662
|
+
const actionsTaken = [];
|
|
663
|
+
let cleanedCount = 0;
|
|
664
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
// Get all worktrees
|
|
667
|
+
const { stdout } = await execAsync('git worktree list --porcelain');
|
|
668
|
+
const worktrees = stdout.trim().split('\n\n');
|
|
669
|
+
for (const worktreeInfo of worktrees) {
|
|
670
|
+
const lines = worktreeInfo.split('\n');
|
|
671
|
+
const worktreePath = lines[0].replace('worktree ', '');
|
|
672
|
+
if (worktreePath === process.cwd())
|
|
673
|
+
continue;
|
|
674
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
675
|
+
const branch = branchLine ? branchLine.replace('branch refs/heads/', '') : 'unknown';
|
|
676
|
+
if (!await fs.pathExists(worktreePath)) {
|
|
677
|
+
actionsTaken.push(`Skipping missing worktree: ${branch}`);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
// Check last modification time
|
|
681
|
+
try {
|
|
682
|
+
const stats = await fs.stat(worktreePath);
|
|
683
|
+
const ageMs = now - stats.mtimeMs;
|
|
684
|
+
if (ageMs > maxAgeMs) {
|
|
685
|
+
actionsTaken.push(`Stale worktree found: ${branch} (${Math.floor(ageMs / (24 * 60 * 60 * 1000))} days old)`);
|
|
686
|
+
// Remove worktree
|
|
687
|
+
await execAsync(`git worktree remove ${worktreePath} --force`);
|
|
688
|
+
cleanedCount++;
|
|
689
|
+
actionsTaken.push(`✓ Removed stale worktree: ${branch}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
actionsTaken.push(`✗ Failed to process ${branch}: ${error}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
action: "cleanup",
|
|
698
|
+
success: true,
|
|
699
|
+
report: `Cleanup completed: ${cleanedCount} stale worktrees removed`,
|
|
700
|
+
actions_taken: actionsTaken,
|
|
701
|
+
worktrees_cleaned: cleanedCount,
|
|
702
|
+
recommendations: cleanedCount > 0 ? ["Review removed worktrees to ensure no important work was lost"] : undefined
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
//# sourceMappingURL=worktreeTools.js.map
|