bobs-workshop 0.3.3 → 3.1.1
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 +2 -2
- package/README.md +199 -210
- package/bin/bobs-workshop.js +109 -0
- package/config/agents.json +27 -0
- package/dist/plugins/bobs-workshop.js +34 -0
- package/dist/tools/background-agent/cancel.d.ts +3 -0
- package/dist/tools/background-agent/cancel.d.ts.map +1 -0
- package/dist/tools/background-agent/cancel.js +52 -0
- package/dist/tools/background-agent/concurrency.d.ts +15 -0
- package/dist/tools/background-agent/concurrency.d.ts.map +1 -0
- package/dist/tools/background-agent/concurrency.js +61 -0
- package/dist/tools/background-agent/index.d.ts +8 -0
- package/dist/tools/background-agent/index.d.ts.map +1 -0
- package/dist/tools/background-agent/index.js +7 -0
- package/dist/tools/background-agent/launch.d.ts +6 -0
- package/dist/tools/background-agent/launch.d.ts.map +1 -0
- package/dist/tools/background-agent/launch.js +33 -0
- package/dist/tools/background-agent/list.d.ts +7 -0
- package/dist/tools/background-agent/list.d.ts.map +1 -0
- package/dist/tools/background-agent/list.js +40 -0
- package/dist/tools/background-agent/manager.d.ts +29 -0
- package/dist/tools/background-agent/manager.d.ts.map +1 -0
- package/dist/tools/background-agent/manager.js +388 -0
- package/dist/tools/background-agent/output.d.ts +3 -0
- package/dist/tools/background-agent/output.d.ts.map +1 -0
- package/dist/tools/background-agent/output.js +41 -0
- package/dist/tools/background-agent/types.d.ts +46 -0
- package/dist/tools/background-agent/types.d.ts.map +1 -0
- package/dist/tools/background-agent/types.js +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/manual/index.d.ts +3 -0
- package/dist/tools/manual/index.d.ts.map +1 -0
- package/dist/tools/manual/index.js +2 -0
- package/dist/tools/manual/manual-update.d.ts +4 -0
- package/dist/tools/manual/manual-update.d.ts.map +1 -0
- package/dist/tools/manual/manual-update.js +190 -0
- package/dist/tools/manual/verify-manual.d.ts +4 -0
- package/dist/tools/manual/verify-manual.d.ts.map +1 -0
- package/dist/tools/manual/verify-manual.js +51 -0
- package/package.json +34 -66
- package/postinstall.js +193 -0
- package/src/agents/alice.md +466 -0
- package/src/agents/bob-rev.md +493 -0
- package/src/agents/bob-send.md +277 -0
- package/src/agents/bob.md +442 -0
- package/src/agents/trace.md +451 -0
- package/src/plugins/bobs-workshop.ts +45 -0
- package/src/skills/api-patterns/SKILL.md +376 -0
- package/src/skills/architecture/SKILL.md +271 -0
- package/src/skills/bobs-workshop/performance/icon.svg +3 -0
- package/src/skills/brainstorming/SKILL.md +210 -0
- package/src/skills/clean-code/SKILL.md +151 -0
- package/src/skills/code-review-checklist/SKILL.md +220 -0
- package/src/skills/database-design/SKILL.md +271 -0
- package/src/skills/exploration/SKILL.md +257 -0
- package/src/skills/frontend-ui-ux/SKILL.md +78 -0
- package/src/skills/git-master/SKILL.md +1105 -0
- package/src/skills/performance/SKILL.md +144 -0
- package/src/skills/performance/icon.svg +3 -0
- package/src/skills/plan-writing/SKILL.md +225 -0
- package/src/skills/security/SKILL.md +410 -0
- package/src/skills/simplification/SKILL.md +238 -0
- package/src/skills/systematic-debugging/SKILL.md +175 -0
- package/src/skills/testing-patterns/SKILL.md +305 -0
- package/src/skills/verification/SKILL.md +286 -0
- package/src/tools/background-agent/cancel.ts +67 -0
- package/src/tools/background-agent/concurrency.ts +71 -0
- package/src/tools/background-agent/index.ts +7 -0
- package/src/tools/background-agent/launch.ts +39 -0
- package/src/tools/background-agent/list.ts +50 -0
- package/src/tools/background-agent/manager.ts +466 -0
- package/src/tools/background-agent/output.ts +57 -0
- package/src/tools/background-agent/types.ts +55 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/manual/index.ts +2 -0
- package/src/tools/manual/manual-update.ts +197 -0
- package/src/tools/manual/verify-manual.ts +60 -0
- package/uninstall.js +64 -0
- package/Claude.md +0 -162
- package/bin/bobs-mcp-server.js +0 -11
- package/bin/bobs-mcp.js +0 -130
- package/dist/api/taskLogger.js +0 -106
- package/dist/api/taskLogger.js.map +0 -1
- package/dist/cli/checker.js +0 -401
- package/dist/cli/checker.js.map +0 -1
- package/dist/cli/cleanup.js +0 -131
- package/dist/cli/cleanup.js.map +0 -1
- package/dist/cli/debug.js +0 -157
- package/dist/cli/debug.js.map +0 -1
- package/dist/cli/health.js +0 -97
- package/dist/cli/health.js.map +0 -1
- package/dist/cli/setup.js +0 -81
- package/dist/cli/setup.js.map +0 -1
- package/dist/cli/workshop.js +0 -42
- package/dist/cli/workshop.js.map +0 -1
- package/dist/dashboard/server.js +0 -1203
- package/dist/dashboard/server.js.map +0 -1
- package/dist/index.js +0 -960
- package/dist/index.js.map +0 -1
- package/dist/prompts/architect.js +0 -221
- package/dist/prompts/architect.js.map +0 -1
- package/dist/prompts/debugger.js +0 -257
- package/dist/prompts/debugger.js.map +0 -1
- package/dist/prompts/engineer.js +0 -249
- package/dist/prompts/engineer.js.map +0 -1
- package/dist/prompts/orchestrator.js +0 -304
- package/dist/prompts/orchestrator.js.map +0 -1
- package/dist/prompts/reviewer.js +0 -289
- package/dist/prompts/reviewer.js.map +0 -1
- package/dist/services/activitySummarizer.js +0 -388
- package/dist/services/activitySummarizer.js.map +0 -1
- package/dist/services/changeValidator.js +0 -396
- package/dist/services/changeValidator.js.map +0 -1
- package/dist/services/claudeOrchestrator.js +0 -343
- package/dist/services/claudeOrchestrator.js.map +0 -1
- package/dist/services/fileMonitor.js +0 -250
- package/dist/services/fileMonitor.js.map +0 -1
- package/dist/services/implementationSummarizer.js +0 -306
- package/dist/services/implementationSummarizer.js.map +0 -1
- package/dist/services/liveMonitor.js +0 -315
- package/dist/services/liveMonitor.js.map +0 -1
- package/dist/services/mcpAuditLogger.js +0 -104
- package/dist/services/mcpAuditLogger.js.map +0 -1
- package/dist/services/mcpLogger.js +0 -223
- package/dist/services/mcpLogger.js.map +0 -1
- package/dist/services/tmuxManager.js +0 -541
- package/dist/services/tmuxManager.js.map +0 -1
- package/dist/tools/approvalTools.js +0 -244
- package/dist/tools/approvalTools.js.map +0 -1
- package/dist/tools/autoDebugger.js +0 -147
- package/dist/tools/autoDebugger.js.map +0 -1
- package/dist/tools/cleanupService.js +0 -221
- package/dist/tools/cleanupService.js.map +0 -1
- package/dist/tools/dashboardTools.js +0 -342
- package/dist/tools/dashboardTools.js.map +0 -1
- package/dist/tools/developmentNudges.js +0 -336
- package/dist/tools/developmentNudges.js.map +0 -1
- package/dist/tools/gitTools.js +0 -741
- package/dist/tools/gitTools.js.map +0 -1
- package/dist/tools/orchestratorTools.js +0 -832
- package/dist/tools/orchestratorTools.js.map +0 -1
- package/dist/tools/searchCache.js +0 -64
- package/dist/tools/searchCache.js.map +0 -1
- package/dist/tools/searchTools.js +0 -1107
- package/dist/tools/searchTools.js.map +0 -1
- package/dist/tools/semgrep-patterns.js +0 -296
- package/dist/tools/semgrep-patterns.js.map +0 -1
- package/dist/tools/specTools.js +0 -332
- package/dist/tools/specTools.js.map +0 -1
- package/dist/tools/structural/__tests__/orchestrator.test.js +0 -61
- package/dist/tools/structural/__tests__/orchestrator.test.js.map +0 -1
- package/dist/tools/structural/cache.js +0 -226
- package/dist/tools/structural/cache.js.map +0 -1
- package/dist/tools/structural/engines/python/index.js +0 -118
- package/dist/tools/structural/engines/python/index.js.map +0 -1
- package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js +0 -97
- package/dist/tools/structural/engines/typescript/__tests__/typescript-engine.test.js.map +0 -1
- package/dist/tools/structural/engines/typescript/analyzer.js +0 -433
- package/dist/tools/structural/engines/typescript/analyzer.js.map +0 -1
- package/dist/tools/structural/engines/typescript/index.js +0 -381
- package/dist/tools/structural/engines/typescript/index.js.map +0 -1
- package/dist/tools/structural/engines/typescript/utils.js +0 -279
- package/dist/tools/structural/engines/typescript/utils.js.map +0 -1
- package/dist/tools/structural/index.js +0 -248
- package/dist/tools/structural/index.js.map +0 -1
- package/dist/tools/structural/types.js +0 -18
- package/dist/tools/structural/types.js.map +0 -1
- package/dist/tools/tmuxTools.js +0 -100
- package/dist/tools/tmuxTools.js.map +0 -1
- package/dist/tools/workRecorder.js +0 -215
- package/dist/tools/workRecorder.js.map +0 -1
- package/dist/tools/worktreeTools.js +0 -705
- package/dist/tools/worktreeTools.js.map +0 -1
- package/dist/utils/__tests__/integration.test.js +0 -57
- package/dist/utils/__tests__/integration.test.js.map +0 -1
- package/dist/utils/__tests__/serverDetection.test.js +0 -151
- package/dist/utils/__tests__/serverDetection.test.js.map +0 -1
- package/dist/utils/errorHandling.js +0 -336
- package/dist/utils/errorHandling.js.map +0 -1
- package/dist/utils/processManager.js +0 -172
- package/dist/utils/processManager.js.map +0 -1
- package/dist/utils/reliability.js +0 -263
- package/dist/utils/reliability.js.map +0 -1
- package/dist/utils/responseFormatter.js +0 -250
- package/dist/utils/responseFormatter.js.map +0 -1
- package/dist/utils/serverDetection.js +0 -133
- package/dist/utils/serverDetection.js.map +0 -1
- package/dist/utils/specMigration.js +0 -105
- package/dist/utils/specMigration.js.map +0 -1
- package/dist/validation/schemas.js +0 -299
- package/dist/validation/schemas.js.map +0 -1
- package/public/.well-known/mcp/manifest.json +0 -473
- package/public/index.html +0 -3157
- package/public/index.html.backup +0 -2805
- package/public/index.html.backup2 +0 -1292
- package/scripts/cleanup-system-logs.ts +0 -121
- package/scripts/init-workspace.js +0 -63
- package/scripts/install-search-tools.js +0 -116
package/dist/tools/gitTools.js
DELETED
|
@@ -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
|