@itz4blitz/agentful 1.8.0 → 1.8.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/bin/hooks/ensure-worktree.js +234 -0
- package/bin/hooks/worktree-service.js +514 -0
- package/package.json +2 -1
- package/template/.claude/agents/backend.md +19 -0
- package/template/.claude/agents/frontend.md +19 -0
- package/template/.claude/agents/orchestrator.md +182 -0
- package/template/.claude/agents/reviewer.md +18 -0
- package/template/.claude/commands/agentful-worktree.md +106 -0
- package/template/.claude/settings.json +13 -1
- package/template/CLAUDE.md +27 -0
- package/version.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensure Worktree Hook
|
|
5
|
+
*
|
|
6
|
+
* PreToolUse hook that enforces git worktree usage for file modifications.
|
|
7
|
+
*
|
|
8
|
+
* Modes:
|
|
9
|
+
* off - Allow all edits (backward compatible)
|
|
10
|
+
* block - Require existing worktree, reject if not in one
|
|
11
|
+
* auto - Create worktree automatically if not in one
|
|
12
|
+
*
|
|
13
|
+
* Environment Variables:
|
|
14
|
+
* AGENTFUL_WORKTREE_MODE - off|block|auto (default: auto)
|
|
15
|
+
* AGENTFUL_WORKTREE_DIR - Where to create worktrees (default: ../)
|
|
16
|
+
* AGENTFUL_WORKTREE_AUTO_CLEANUP - Auto-remove after completion (default: true)
|
|
17
|
+
* AGENTFUL_WORKTREE_RETENTION_DAYS - Days before cleanup (default: 7)
|
|
18
|
+
* AGENTFUL_WORKTREE_MAX_ACTIVE - Max active worktrees (default: 5)
|
|
19
|
+
*
|
|
20
|
+
* To disable this hook:
|
|
21
|
+
* Temporary: export AGENTFUL_WORKTREE_MODE=off
|
|
22
|
+
* Permanent: Remove from .claude/settings.json PreToolUse hooks
|
|
23
|
+
* Customize: Edit bin/hooks/ensure-worktree.js
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from 'fs';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import { execSync } from 'child_process';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
31
|
+
|
|
32
|
+
// Get configuration
|
|
33
|
+
const MODE = process.env.AGENTFUL_WORKTREE_MODE || 'auto';
|
|
34
|
+
const WORKTREE_DIR = process.env.AGENTFUL_WORKTREE_DIR || '../';
|
|
35
|
+
const AUTO_CLEANUP = process.env.AGENTFUL_WORKTREE_AUTO_CLEANUP !== 'false';
|
|
36
|
+
const RETENTION_DAYS = parseInt(process.env.AGENTFUL_WORKTREE_RETENTION_DAYS || '7', 10);
|
|
37
|
+
const MAX_ACTIVE = parseInt(process.env.AGENTFUL_WORKTREE_MAX_ACTIVE || '5', 10);
|
|
38
|
+
const AGENT_TYPE = process.env.AGENTFUL_AGENT_TYPE || 'general';
|
|
39
|
+
const TASK_TYPE = process.env.AGENTFUL_TASK_TYPE || 'general';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect if currently in a git worktree
|
|
43
|
+
*/
|
|
44
|
+
function isInWorktree() {
|
|
45
|
+
try {
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const gitFile = path.join(cwd, '.git');
|
|
48
|
+
|
|
49
|
+
// If .git is a file (not a directory), we're in a worktree
|
|
50
|
+
if (existsSync(gitFile) && statSync(gitFile).isFile()) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Determine worktree purpose from agent/task context
|
|
60
|
+
*/
|
|
61
|
+
function determinePurpose() {
|
|
62
|
+
// Agent-based purposes
|
|
63
|
+
const agentPurposes = {
|
|
64
|
+
'fixer': 'fix',
|
|
65
|
+
'reviewer': 'review',
|
|
66
|
+
'tester': 'test',
|
|
67
|
+
'backend': 'backend',
|
|
68
|
+
'frontend': 'frontend',
|
|
69
|
+
'architect': 'architect',
|
|
70
|
+
'orchestrator': 'orchestrator',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Task-based purposes
|
|
74
|
+
const taskPurposes = {
|
|
75
|
+
'feature': 'feature',
|
|
76
|
+
'hotfix': 'hotfix',
|
|
77
|
+
'bugfix': 'bugfix',
|
|
78
|
+
'experiment': 'experiment',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Determine purpose based on agent type, then task type
|
|
82
|
+
if (agentPurposes[AGENT_TYPE]) {
|
|
83
|
+
return agentPurposes[AGENT_TYPE];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (taskPurposes[TASK_TYPE]) {
|
|
87
|
+
return taskPurposes[TASK_TYPE];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Default to general
|
|
91
|
+
return 'general';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Show block message with helpful error
|
|
96
|
+
*/
|
|
97
|
+
function showBlockMessage() {
|
|
98
|
+
const branch = getCurrentBranch();
|
|
99
|
+
|
|
100
|
+
console.error(`
|
|
101
|
+
:::error
|
|
102
|
+
═════════════════════════════════════════════════
|
|
103
|
+
🚫 Blocked: Direct Repository Edits
|
|
104
|
+
═════════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
You are attempting to edit files in the root repository, but AGENTFUL_WORKTREE_MODE
|
|
107
|
+
is set to "block" which requires working in a git worktree.
|
|
108
|
+
|
|
109
|
+
Current branch: ${branch}
|
|
110
|
+
Current directory: ${process.cwd()}
|
|
111
|
+
|
|
112
|
+
To proceed, choose one option:
|
|
113
|
+
|
|
114
|
+
1. 🌳 Create a worktree (recommended):
|
|
115
|
+
git worktree add ../my-worktree -b ${branch}
|
|
116
|
+
cd ../my-worktree
|
|
117
|
+
|
|
118
|
+
2. ⚙️ Change mode to auto (creates worktrees automatically):
|
|
119
|
+
export AGENTFUL_WORKTREE_MODE=auto
|
|
120
|
+
|
|
121
|
+
3. 🚫 Disable worktree protection (not recommended):
|
|
122
|
+
export AGENTFUL_WORKTREE_MODE=off
|
|
123
|
+
|
|
124
|
+
For more information, see: /agentful-worktree or docs/pages/concepts/git-worktrees.mdx
|
|
125
|
+
:::`);
|
|
126
|
+
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create worktree automatically
|
|
132
|
+
*/
|
|
133
|
+
function createWorktree() {
|
|
134
|
+
const repoRoot = findRepoRoot();
|
|
135
|
+
if (!repoRoot) {
|
|
136
|
+
console.error(':::error Not in a git repository:::');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const purpose = determinePurpose();
|
|
141
|
+
const branch = getCurrentBranch();
|
|
142
|
+
const timestamp = Date.now();
|
|
143
|
+
|
|
144
|
+
// Generate worktree name
|
|
145
|
+
const worktreeName = `agentful-${purpose}-${sanitizeBranchName(branch)}-${timestamp}`;
|
|
146
|
+
|
|
147
|
+
// Create worktree
|
|
148
|
+
const worktreePath = path.join(repoRoot, WORKTREE_DIR, worktreeName);
|
|
149
|
+
|
|
150
|
+
console.log(`🌳 Creating worktree: ${worktreeName}`);
|
|
151
|
+
console.log(` Branch: ${branch}`);
|
|
152
|
+
console.log(` Path: ${worktreePath}`);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
execSync(
|
|
156
|
+
`git worktree add "${worktreePath}" -b "${branch}"`,
|
|
157
|
+
{ cwd: repoRoot, stdio: 'inherit' }
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`:::error Failed to create worktree: ${error.message}:::`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Track in state.json (would happen in orchestrator, but this is standalone)
|
|
165
|
+
try {
|
|
166
|
+
const stateFile = path.join(repoRoot, '.agentful', 'state.json');
|
|
167
|
+
if (existsSync(stateFile)) {
|
|
168
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf8'));
|
|
169
|
+
state.current_worktree = {
|
|
170
|
+
name: worktreeName,
|
|
171
|
+
path: worktreePath,
|
|
172
|
+
branch: branch,
|
|
173
|
+
purpose: purpose,
|
|
174
|
+
created_at: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// State file might not exist yet, that's okay
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Export for caller to capture
|
|
183
|
+
console.log(`export AGENTFUL_WORKTREE_DIR="${worktreePath}"`);
|
|
184
|
+
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Main execution
|
|
190
|
+
*/
|
|
191
|
+
(() => {
|
|
192
|
+
// Get tool and file from environment
|
|
193
|
+
const tool = process.env.TOOL || '';
|
|
194
|
+
const file = process.env.FILE || '';
|
|
195
|
+
|
|
196
|
+
// Only check Write and Edit tools
|
|
197
|
+
if (tool !== 'Write' && tool !== 'Edit') {
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Auto-disable in CI environments
|
|
202
|
+
if (isCIEnvironment()) {
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if already in worktree (explicitly set or detected)
|
|
207
|
+
if (WORKTREE_DIR) {
|
|
208
|
+
// In worktree - allow operation
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Not in worktree - handle based on mode
|
|
213
|
+
switch (MODE) {
|
|
214
|
+
case 'off':
|
|
215
|
+
// Allow all edits silently
|
|
216
|
+
process.exit(0);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case 'block':
|
|
220
|
+
// Require existing worktree
|
|
221
|
+
showBlockMessage();
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
case 'auto':
|
|
225
|
+
// Create worktree automatically
|
|
226
|
+
createWorktree();
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
default:
|
|
230
|
+
console.error(`:::warning Invalid AGENTFUL_WORKTREE_MODE: ${MODE}:::`);
|
|
231
|
+
console.error(`:::warning Valid options: off, block, auto:::`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Git Worktree Service
|
|
5
|
+
*
|
|
6
|
+
* Core service for managing git worktrees in agentful.
|
|
7
|
+
* Provides worktree creation, tracking, and cleanup functions.
|
|
8
|
+
*
|
|
9
|
+
* Environment Variables:
|
|
10
|
+
* AGENTFUL_WORKTREE_MODE - auto|block|off (default: auto)
|
|
11
|
+
* AGENTFUL_WORKTREE_LOCATION - Where to create worktrees (default: ../)
|
|
12
|
+
* AGENTFUL_WORKTREE_AUTO_CLEANUP - Auto-remove after completion (default: true)
|
|
13
|
+
* AGENTFUL_WORKTREE_RETENTION_DAYS - Days before cleanup (default: 7)
|
|
14
|
+
* AGENTFUL_WORKTREE_MAX_ACTIVE - Max active worktrees (default: 5)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node worktree-service.js create <purpose> <branch>
|
|
18
|
+
* node worktree-service.js list
|
|
19
|
+
* node worktree-service.js cleanup
|
|
20
|
+
* node worktree-service.js prune
|
|
21
|
+
* node worktree-service.js status
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
// Configuration from environment
|
|
33
|
+
const CONFIG = {
|
|
34
|
+
mode: process.env.AGENTFUL_WORKTREE_MODE || 'auto',
|
|
35
|
+
location: process.env.AGENTFUL_WORKTREE_LOCATION || '../',
|
|
36
|
+
autoCleanup: process.env.AGENTFUL_WORKTREE_AUTO_CLEANUP !== 'false',
|
|
37
|
+
retentionDays: parseInt(process.env.AGENTFUL_WORKTREE_RETENTION_DAYS || '7', 10),
|
|
38
|
+
maxActive: parseInt(process.env.AGENTFUL_WORKTREE_MAX_ACTIVE || '5', 10),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Paths
|
|
42
|
+
const REPO_ROOT = findRepoRoot();
|
|
43
|
+
const WORKTREES_DIR = path.join(REPO_ROOT, '.git', 'worktrees');
|
|
44
|
+
const TRACKING_FILE = path.join(REPO_ROOT, '.agentful', 'worktrees.json');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find git repository root
|
|
48
|
+
*/
|
|
49
|
+
function findRepoRoot() {
|
|
50
|
+
try {
|
|
51
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
cwd: process.cwd(),
|
|
54
|
+
}).trim();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('❌ Not in a git repository');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current git branch
|
|
63
|
+
*/
|
|
64
|
+
function getCurrentBranch() {
|
|
65
|
+
try {
|
|
66
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
}).trim();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return 'unknown';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get current working directory relative to repo root
|
|
77
|
+
*/
|
|
78
|
+
function getCurrentWorktree() {
|
|
79
|
+
const cwd = process.cwd();
|
|
80
|
+
const gitCommonDir = execSync('git rev-parse --git-common-dir', {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
cwd,
|
|
83
|
+
}).trim();
|
|
84
|
+
|
|
85
|
+
// If .git is a file, we're in a worktree
|
|
86
|
+
const gitFile = path.join(cwd, '.git');
|
|
87
|
+
const isWorktree = fs.existsSync(gitFile) && fs.statSync(gitFile).isFile();
|
|
88
|
+
|
|
89
|
+
if (isWorktree) {
|
|
90
|
+
// Read the worktree path from .git file
|
|
91
|
+
const gitDir = fs.readFileSync(gitFile, 'utf8').trim();
|
|
92
|
+
const worktreePath = gitDir.replace('gitdir: ', '').replace('/.git', '');
|
|
93
|
+
return path.basename(worktreePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sanitize branch name for use in worktree name
|
|
101
|
+
*/
|
|
102
|
+
function sanitizeBranchName(branch) {
|
|
103
|
+
return branch
|
|
104
|
+
.replace(/\//g, '-') // Replace / with -
|
|
105
|
+
.replace(/[^a-zA-Z0-9-]/g, '') // Remove special chars
|
|
106
|
+
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
107
|
+
.replace(/^-|-$/g, ''); // Trim leading/trailing dashes
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate worktree name for security
|
|
112
|
+
*/
|
|
113
|
+
function validateWorktreeName(name) {
|
|
114
|
+
// Prevent path traversal
|
|
115
|
+
const resolvedPath = path.resolve(REPO_ROOT, CONFIG.location, name);
|
|
116
|
+
if (!resolvedPath.startsWith(REPO_ROOT)) {
|
|
117
|
+
throw new Error('Invalid worktree name: path traversal detected');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for valid characters
|
|
121
|
+
if (!/^[a-zA-Z0-9-]+$/.test(name)) {
|
|
122
|
+
throw new Error('Invalid worktree name: contains invalid characters');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get tracked worktrees from .agentful/worktrees.json
|
|
130
|
+
*/
|
|
131
|
+
function getTrackedWorktrees() {
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(TRACKING_FILE)) {
|
|
134
|
+
const content = fs.readFileSync(TRACKING_FILE, 'utf8');
|
|
135
|
+
return JSON.parse(content);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Invalid tracking file, start fresh
|
|
139
|
+
console.warn(`⚠️ Corrupted tracking file, starting fresh: ${TRACKING_FILE}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { active: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save tracked worktrees to .agentful/worktrees.json
|
|
147
|
+
*/
|
|
148
|
+
function saveTrackedWorktrees(data) {
|
|
149
|
+
const dir = path.dirname(TRACKING_FILE);
|
|
150
|
+
if (!fs.existsSync(dir)) {
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
TRACKING_FILE,
|
|
156
|
+
JSON.stringify(data, null, 2),
|
|
157
|
+
'utf8'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get list of all git worktrees
|
|
163
|
+
*/
|
|
164
|
+
function getGitWorktrees() {
|
|
165
|
+
try {
|
|
166
|
+
const output = execSync('git worktree list', {
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
cwd: REPO_ROOT,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return output.trim().split('\n').map(line => {
|
|
172
|
+
const [worktreePath, commit, branch] = line.split(/\s+/);
|
|
173
|
+
return {
|
|
174
|
+
path: worktreePath,
|
|
175
|
+
commit,
|
|
176
|
+
branch: branch.replace(/[\[\]]/g, ''), // Remove [ ] brackets
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a branch is already checked out
|
|
186
|
+
*/
|
|
187
|
+
function isBranchCheckedOut(branch) {
|
|
188
|
+
const worktrees = getGitWorktrees();
|
|
189
|
+
return worktrees.some(w => w.branch === branch);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a unique branch name if original is already checked out
|
|
194
|
+
*/
|
|
195
|
+
function getUniqueBranch(originalBranch, timestamp) {
|
|
196
|
+
if (!isBranchCheckedOut(originalBranch)) {
|
|
197
|
+
return originalBranch;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Create unique branch with timestamp
|
|
201
|
+
const uniqueName = `${originalBranch}-agentful-${timestamp}`;
|
|
202
|
+
console.log(`⚠️ Branch "${originalBranch}" already checked out`);
|
|
203
|
+
console.log(` Creating unique branch: ${uniqueName}`);
|
|
204
|
+
return uniqueName;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check disk space
|
|
209
|
+
*/
|
|
210
|
+
function checkDiskSpace(requiredBytes) {
|
|
211
|
+
try {
|
|
212
|
+
const stats = fs.statSync(REPO_ROOT);
|
|
213
|
+
// Note: This is a basic check. For production, use df or similar
|
|
214
|
+
return true;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.warn('⚠️ Could not check disk space');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a new worktree
|
|
223
|
+
*/
|
|
224
|
+
function createWorktree(purpose, branch = null) {
|
|
225
|
+
const timestamp = Date.now();
|
|
226
|
+
const currentBranch = branch || getCurrentBranch();
|
|
227
|
+
const sanitizedBranch = sanitizeBranchName(currentBranch);
|
|
228
|
+
|
|
229
|
+
// Generate worktree name
|
|
230
|
+
const worktreeName = `agentful-${purpose}-${sanitizedBranch}-${timestamp}`;
|
|
231
|
+
|
|
232
|
+
// Validate
|
|
233
|
+
validateWorktreeName(worktreeName);
|
|
234
|
+
|
|
235
|
+
// Get unique branch if needed
|
|
236
|
+
const targetBranch = getUniqueBranch(currentBranch, timestamp);
|
|
237
|
+
|
|
238
|
+
// Check max active worktrees
|
|
239
|
+
const tracked = getTrackedWorktrees();
|
|
240
|
+
if (tracked.active.length >= CONFIG.maxActive) {
|
|
241
|
+
console.warn(`⚠️ Maximum active worktrees reached (${CONFIG.maxActive})`);
|
|
242
|
+
console.warn(' Run cleanup first: node worktree-service.js cleanup');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create worktree
|
|
246
|
+
const worktreePath = path.join(REPO_ROOT, CONFIG.location, worktreeName);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
console.log(`🌳 Creating worktree: ${worktreeName}`);
|
|
250
|
+
console.log(` Branch: ${targetBranch}`);
|
|
251
|
+
console.log(` Path: ${worktreePath}`);
|
|
252
|
+
|
|
253
|
+
execSync(
|
|
254
|
+
`git worktree add "${worktreePath}" -b "${targetBranch}"`,
|
|
255
|
+
{ cwd: REPO_ROOT, stdio: 'inherit' }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Initialize submodules if present
|
|
259
|
+
const submodulePath = path.join(worktreePath, '.gitmodules');
|
|
260
|
+
if (fs.existsSync(submodulePath)) {
|
|
261
|
+
console.log(' 📦 Initializing submodules...');
|
|
262
|
+
execSync('git submodule update --init --recursive', {
|
|
263
|
+
cwd: worktreePath,
|
|
264
|
+
stdio: 'inherit',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Track the worktree
|
|
269
|
+
const worktreeData = {
|
|
270
|
+
name: worktreeName,
|
|
271
|
+
path: worktreePath,
|
|
272
|
+
branch: targetBranch,
|
|
273
|
+
purpose,
|
|
274
|
+
created_at: new Date().toISOString(),
|
|
275
|
+
last_activity: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
tracked.active.push(worktreeData);
|
|
279
|
+
saveTrackedWorktrees(tracked);
|
|
280
|
+
|
|
281
|
+
console.log(`✅ Worktree created successfully`);
|
|
282
|
+
console.log(` Export AGENTFUL_WORKTREE_DIR="${worktreePath}"`);
|
|
283
|
+
|
|
284
|
+
// Output worktree path for easy capture
|
|
285
|
+
console.log(`WORKTREE_PATH=${worktreePath}`);
|
|
286
|
+
|
|
287
|
+
return worktreeData;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`❌ Failed to create worktree: ${error.message}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* List all tracked worktrees
|
|
296
|
+
*/
|
|
297
|
+
function listWorktrees() {
|
|
298
|
+
const tracked = getTrackedWorktrees();
|
|
299
|
+
const gitWorktrees = getGitWorktrees();
|
|
300
|
+
|
|
301
|
+
console.log('\n📋 Active Worktrees:\n');
|
|
302
|
+
|
|
303
|
+
if (tracked.active.length === 0) {
|
|
304
|
+
console.log(' No active worktrees\n');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
tracked.active.forEach(w => {
|
|
309
|
+
const gitWt = gitWorktrees.find(gw => gw.path === w.path);
|
|
310
|
+
const exists = !!gitWt;
|
|
311
|
+
const ageMs = Date.now() - new Date(w.created_at).getTime();
|
|
312
|
+
const ageMins = Math.floor(ageMs / 60000);
|
|
313
|
+
|
|
314
|
+
let status = exists ? '🟢 Active' : '🔴 Orphaned';
|
|
315
|
+
if (ageMins > 60) status = '🟡 Stale';
|
|
316
|
+
|
|
317
|
+
console.log(`┌─────────────────────────────────────────────────────────────┐`);
|
|
318
|
+
console.log(`│ ${w.name}`);
|
|
319
|
+
console.log(`│ ├─ Branch: ${w.branch}`);
|
|
320
|
+
console.log(`│ ├─ Purpose: ${w.purpose}`);
|
|
321
|
+
console.log(`│ ├─ Path: ${w.path}`);
|
|
322
|
+
console.log(`│ ├─ Status: ${status} (${ageMins} min ago)`);
|
|
323
|
+
console.log(`│ └─ Created: ${w.created_at}`);
|
|
324
|
+
console.log(`└─────────────────────────────────────────────────────────────┘`);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get status of current session
|
|
332
|
+
*/
|
|
333
|
+
function getStatus() {
|
|
334
|
+
const currentWorktree = getCurrentWorktree();
|
|
335
|
+
const tracked = getTrackedWorktrees();
|
|
336
|
+
|
|
337
|
+
if (currentWorktree) {
|
|
338
|
+
const wt = tracked.active.find(w => w.name === currentWorktree);
|
|
339
|
+
if (wt) {
|
|
340
|
+
console.log(`\n🌳 Current worktree: ${wt.name}`);
|
|
341
|
+
console.log(` Branch: ${wt.branch}`);
|
|
342
|
+
console.log(` Purpose: ${wt.purpose}`);
|
|
343
|
+
console.log(` Path: ${wt.path}\n`);
|
|
344
|
+
} else {
|
|
345
|
+
console.log(`\n⚠️ In untracked worktree: ${currentWorktree}`);
|
|
346
|
+
console.log(` Worktree exists but not tracked by agentful\n`);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
console.log('\n📍 Working in root repository');
|
|
350
|
+
console.log(' No active worktree for this session\n');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`Mode: ${CONFIG.mode}`);
|
|
354
|
+
console.log(`Location: ${CONFIG.location}`);
|
|
355
|
+
console.log(`Auto-cleanup: ${CONFIG.autoCleanup ? 'enabled' : 'disabled'}`);
|
|
356
|
+
console.log(`Max active: ${CONFIG.maxActive}\n`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Cleanup stale worktrees
|
|
361
|
+
*/
|
|
362
|
+
function cleanupWorktrees() {
|
|
363
|
+
const tracked = getTrackedWorktrees();
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
const retentionMs = CONFIG.retentionDays * 24 * 60 * 60 * 1000;
|
|
366
|
+
let cleanedCount = 0;
|
|
367
|
+
|
|
368
|
+
console.log(`\n🧹 Cleaning up worktrees older than ${CONFIG.retentionDays} days...\n`);
|
|
369
|
+
|
|
370
|
+
const active = tracked.active.filter(w => {
|
|
371
|
+
const age = now - new Date(w.created_at).getTime();
|
|
372
|
+
const isStale = age > retentionMs;
|
|
373
|
+
|
|
374
|
+
if (isStale) {
|
|
375
|
+
console.log(`🗑️ Removing stale: ${w.name}`);
|
|
376
|
+
try {
|
|
377
|
+
execSync(`git worktree remove "${w.path}"`, {
|
|
378
|
+
cwd: REPO_ROOT,
|
|
379
|
+
stdio: 'inherit',
|
|
380
|
+
});
|
|
381
|
+
cleanedCount++;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.warn(` ⚠️ Failed to remove: ${error.message}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return !isStale; // Keep only non-stale
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Save updated tracking
|
|
391
|
+
saveTrackedWorktrees({ active });
|
|
392
|
+
|
|
393
|
+
// Also run git prune to clean metadata
|
|
394
|
+
console.log('\n🧹 Pruning git worktree metadata...');
|
|
395
|
+
try {
|
|
396
|
+
execSync('git worktree prune', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.warn(` ⚠️ Prune warning: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
console.log(`\n✅ Cleanup complete. Removed ${cleanedCount} worktree(s).\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Prune git worktree metadata
|
|
406
|
+
*/
|
|
407
|
+
function pruneWorktrees() {
|
|
408
|
+
console.log('\n🧹 Pruning git worktree metadata...\n');
|
|
409
|
+
try {
|
|
410
|
+
execSync('git worktree prune', { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
411
|
+
console.log('✅ Prune complete\n');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error(`❌ Prune failed: ${error.message}`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Remove a specific worktree
|
|
420
|
+
*/
|
|
421
|
+
function removeWorktree(worktreeName) {
|
|
422
|
+
const tracked = getTrackedWorktrees();
|
|
423
|
+
const worktree = tracked.active.find(w => w.name === worktreeName);
|
|
424
|
+
|
|
425
|
+
if (!worktree) {
|
|
426
|
+
console.error(`❌ Worktree not found: ${worktreeName}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
console.log(`🗑️ Removing worktree: ${worktreeName}`);
|
|
432
|
+
execSync(`git worktree remove "${worktree.path}"`, {
|
|
433
|
+
cwd: REPO_ROOT,
|
|
434
|
+
stdio: 'inherit',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Remove from tracking
|
|
438
|
+
const active = tracked.active.filter(w => w.name !== worktreeName);
|
|
439
|
+
saveTrackedWorktrees({ active });
|
|
440
|
+
|
|
441
|
+
console.log('✅ Worktree removed\n');
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error(`❌ Failed to remove worktree: ${error.message}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Detect if running in CI environment
|
|
450
|
+
*/
|
|
451
|
+
function isCIEnvironment() {
|
|
452
|
+
return process.env.CI === 'true' ||
|
|
453
|
+
process.env.GITHUB_ACTIONS === 'true' ||
|
|
454
|
+
process.env.GITLAB_CI === 'true' ||
|
|
455
|
+
process.env.CIRCLECI === 'true';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// CLI interface
|
|
459
|
+
const command = process.argv[2];
|
|
460
|
+
const args = process.argv.slice(3);
|
|
461
|
+
|
|
462
|
+
// Auto-disable worktree mode in CI
|
|
463
|
+
if (isCIEnvironment()) {
|
|
464
|
+
console.log('🤖 CI environment detected. Worktree mode auto-disabled.\n');
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
switch (command) {
|
|
469
|
+
case 'create':
|
|
470
|
+
if (args.length < 1) {
|
|
471
|
+
console.error('Usage: worktree-service.js create <purpose> [branch]');
|
|
472
|
+
console.error('Example: worktree-service.js create feature feature/auth');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
createWorktree(args[0], args[1]);
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
case 'list':
|
|
479
|
+
listWorktrees();
|
|
480
|
+
break;
|
|
481
|
+
|
|
482
|
+
case 'status':
|
|
483
|
+
getStatus();
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'cleanup':
|
|
487
|
+
cleanupWorktrees();
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'prune':
|
|
491
|
+
pruneWorktrees();
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
case 'remove':
|
|
495
|
+
if (args.length < 1) {
|
|
496
|
+
console.error('Usage: worktree-service.js remove <worktree-name>');
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
removeWorktree(args[0]);
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
default:
|
|
503
|
+
console.log('Git Worktree Service for agentful\n');
|
|
504
|
+
console.log('Usage: node worktree-service.js <command> [args]\n');
|
|
505
|
+
console.log('Commands:');
|
|
506
|
+
console.log(' create <purpose> [branch] Create a new worktree');
|
|
507
|
+
console.log(' list List all tracked worktrees');
|
|
508
|
+
console.log(' status Show current worktree status');
|
|
509
|
+
console.log(' cleanup Remove stale worktrees');
|
|
510
|
+
console.log(' prune Prune git worktree metadata');
|
|
511
|
+
console.log(' remove <name> Remove specific worktree\n');
|
|
512
|
+
console.log(`Environment: AGENTFUL_WORKTREE_MODE=${CONFIG.mode}`);
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itz4blitz/agentful",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Pre-configured AI toolkit with self-hosted execution - works with any LLM, any tech stack, any platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"@vitest/coverage-v8": "^1.2.0",
|
|
80
80
|
"eslint": "^9.39.2",
|
|
81
81
|
"semantic-release": "^25.0.2",
|
|
82
|
+
"vite-plugin-pagefind": "^1.0.7",
|
|
82
83
|
"vitest": "^1.2.0",
|
|
83
84
|
"vocs": "^1.4.1"
|
|
84
85
|
},
|
|
@@ -48,6 +48,25 @@ You are the **Backend Agent**. You implement server-side code using clean archit
|
|
|
48
48
|
|
|
49
49
|
**Reference skills for tech-specific guidance:**
|
|
50
50
|
- Look in `.claude/skills/` for framework-specific patterns
|
|
51
|
+
|
|
52
|
+
## Step 1.5: Worktree Check
|
|
53
|
+
|
|
54
|
+
Before implementing, verify your working environment:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Check if AGENTFUL_WORKTREE_DIR is set
|
|
58
|
+
if exists("$AGENTFUL_WORKTREE_DIR"):
|
|
59
|
+
worktree_path = "$AGENTFUL_WORKTREE_DIR"
|
|
60
|
+
echo "✅ Implementing backend in worktree: $worktree_path"
|
|
61
|
+
else:
|
|
62
|
+
echo "📍 Implementing backend in root repository"
|
|
63
|
+
echo "⚠️ Changes will affect main branch directly"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Report worktree status**: In your final report, always include:
|
|
67
|
+
- Worktree path (if applicable)
|
|
68
|
+
- Branch being worked on
|
|
69
|
+
- Any commits created
|
|
51
70
|
- Skills contain project-specific conventions, not generic framework docs
|
|
52
71
|
|
|
53
72
|
**Sample existing code to understand conventions:**
|
|
@@ -53,6 +53,25 @@ You are the **Frontend Agent**. You implement user interfaces and client-side co
|
|
|
53
53
|
|
|
54
54
|
**Reference skills for tech-specific guidance:**
|
|
55
55
|
- Look in `.claude/skills/` for framework-specific patterns
|
|
56
|
+
|
|
57
|
+
## Step 1.5: Worktree Check
|
|
58
|
+
|
|
59
|
+
Before implementing, verify your working environment:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Check if AGENTFUL_WORKTREE_DIR is set
|
|
63
|
+
if exists("$AGENTFUL_WORKTREE_DIR"):
|
|
64
|
+
worktree_path = "$AGENTFUL_WORKTREE_DIR"
|
|
65
|
+
echo "✅ Implementing frontend in worktree: $worktree_path"
|
|
66
|
+
else:
|
|
67
|
+
echo "📍 Implementing frontend in root repository"
|
|
68
|
+
echo "⚠️ Changes will affect main branch directly"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Report worktree status**: In your final report, always include:
|
|
72
|
+
- Worktree path (if applicable)
|
|
73
|
+
- Branch being worked on
|
|
74
|
+
- Any commits created
|
|
56
75
|
- Skills contain project-specific conventions (styling, state management, forms)
|
|
57
76
|
|
|
58
77
|
**Sample existing code to understand conventions:**
|
|
@@ -456,6 +456,188 @@ else:
|
|
|
456
456
|
}
|
|
457
457
|
```
|
|
458
458
|
|
|
459
|
+
## Worktree Management
|
|
460
|
+
|
|
461
|
+
Git worktrees provide isolated environments for parallel agent development. Each task can work in its own worktree without polluting the main repository.
|
|
462
|
+
|
|
463
|
+
### Why Worktrees?
|
|
464
|
+
|
|
465
|
+
- **Isolation**: Changes are isolated until merged
|
|
466
|
+
- **Parallel Development**: Multiple agents can work simultaneously
|
|
467
|
+
- **Safety**: Experimental changes don't affect main branch
|
|
468
|
+
- **Clean History**: Each worktree has its own git history
|
|
469
|
+
|
|
470
|
+
### Worktree Modes
|
|
471
|
+
|
|
472
|
+
Controlled via `AGENTFUL_WORKTREE_MODE` environment variable:
|
|
473
|
+
|
|
474
|
+
| Mode | Behavior | When to Use |
|
|
475
|
+
|-------|-----------|--------------|
|
|
476
|
+
| `auto` | Create worktrees automatically | Default, recommended |
|
|
477
|
+
| `block` | Require existing worktree | Strict environments |
|
|
478
|
+
| `off` | Allow direct edits | Legacy, manual control |
|
|
479
|
+
|
|
480
|
+
### Before Delegation: Worktree Setup
|
|
481
|
+
|
|
482
|
+
Before delegating to any specialist agent:
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
# Check worktree mode
|
|
486
|
+
if AGENTFUL_WORKTREE_MODE != "off":
|
|
487
|
+
# Check if we need a worktree
|
|
488
|
+
current_worktree = get_current_worktree()
|
|
489
|
+
|
|
490
|
+
if not current_worktree:
|
|
491
|
+
# Determine purpose from task type
|
|
492
|
+
purpose = determine_worktree_purpose(task_type)
|
|
493
|
+
|
|
494
|
+
# Create worktree via worktree-service
|
|
495
|
+
worktree = execute("node bin/hooks/worktree-service.js create " + purpose)
|
|
496
|
+
|
|
497
|
+
# Set environment for delegated agents
|
|
498
|
+
# They inherit AGENTFUL_WORKTREE_DIR
|
|
499
|
+
current_worktree = worktree
|
|
500
|
+
|
|
501
|
+
# Track in state.json
|
|
502
|
+
state.current_worktree = {
|
|
503
|
+
name: worktree.name,
|
|
504
|
+
path: worktree.path,
|
|
505
|
+
branch: worktree.branch,
|
|
506
|
+
purpose: worktree.purpose,
|
|
507
|
+
created_at: worktree.created_at
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Worktree Naming Convention
|
|
512
|
+
|
|
513
|
+
Worktrees are automatically named:
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
agentful-<purpose>-<branch-slug>-<timestamp>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Examples:
|
|
520
|
+
- `agentful-feature-auth-1739297120` - Feature development
|
|
521
|
+
- `agentful-fix-coverage-1739297150` - Fixer adding coverage
|
|
522
|
+
- `agentful-review-1739297180` - Reviewer validating
|
|
523
|
+
- `agentful-hotfix-login-bug-1739297210` - Hotfix work
|
|
524
|
+
|
|
525
|
+
### State Schema Extension
|
|
526
|
+
|
|
527
|
+
Track worktrees in `.agentful/state.json`:
|
|
528
|
+
|
|
529
|
+
```json
|
|
530
|
+
{
|
|
531
|
+
"current_task": "feature/auth",
|
|
532
|
+
"current_phase": "implementation",
|
|
533
|
+
"current_worktree": {
|
|
534
|
+
"name": "agentful-feature-auth-1739297120",
|
|
535
|
+
"path": "/Users/dev/project/.git/worktrees/agentful-feature-auth-1739297120",
|
|
536
|
+
"branch": "feature/auth",
|
|
537
|
+
"purpose": "feature",
|
|
538
|
+
"created_at": "2025-02-11T15:30:00Z"
|
|
539
|
+
},
|
|
540
|
+
"worktrees": {
|
|
541
|
+
"active": [
|
|
542
|
+
{
|
|
543
|
+
"name": "agentful-feature-auth-1739297120",
|
|
544
|
+
"branch": "feature/auth",
|
|
545
|
+
"purpose": "feature",
|
|
546
|
+
"agent": "orchestrator",
|
|
547
|
+
"created_at": "2025-02-11T15:30:00Z",
|
|
548
|
+
"last_activity": "2025-02-11T16:45:00Z"
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### After Task Completion: Worktree Cleanup
|
|
556
|
+
|
|
557
|
+
When a feature passes all quality gates:
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
if task_completed and AGENTFUL_WORKTREE_AUTO_CLEANUP != "false":
|
|
561
|
+
# Commit changes in worktree
|
|
562
|
+
git -C $WORKTREE_PATH add .
|
|
563
|
+
git -C $WORKTREE_PATH commit -m "feat: complete ${feature_name}"
|
|
564
|
+
|
|
565
|
+
# Ask user what to do next
|
|
566
|
+
response = AskUserQuestion(
|
|
567
|
+
"Feature complete! What would you like to do?",
|
|
568
|
+
options: [
|
|
569
|
+
"Create PR",
|
|
570
|
+
"Merge to main",
|
|
571
|
+
"Keep worktree for review",
|
|
572
|
+
"Clean up worktree"
|
|
573
|
+
]
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
if response == "Create PR" or response == "Merge to main":
|
|
577
|
+
# Push and create PR/merge
|
|
578
|
+
git -C $WORKTREE_PATH push
|
|
579
|
+
# ... PR creation logic ...
|
|
580
|
+
|
|
581
|
+
# Remove worktree
|
|
582
|
+
execute("node bin/hooks/worktree-service.js remove " + worktree.name)
|
|
583
|
+
|
|
584
|
+
# Update state.json
|
|
585
|
+
state.current_worktree = null
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Handling Interruptions
|
|
589
|
+
|
|
590
|
+
If user interrupts during worktree operation:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
on SIGINT:
|
|
594
|
+
if active_worktree:
|
|
595
|
+
# Mark for review instead of deleting
|
|
596
|
+
state.interrupted_worktree = {
|
|
597
|
+
name: active_worktree.name,
|
|
598
|
+
path: active_worktree.path,
|
|
599
|
+
interrupted_at: new Date().toISOString()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log("Worktree preserved: " + active_worktree.name)
|
|
603
|
+
console.log("Run /agentful-worktree --resume to continue")
|
|
604
|
+
console.log("Run /agentful-worktree --cleanup to remove")
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Worktree Service API
|
|
608
|
+
|
|
609
|
+
The `worktree-service.js` provides these operations:
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
node bin/hooks/worktree-service.js create <purpose> [branch] # Create worktree
|
|
613
|
+
node bin/hooks/worktree-service.js list # List all worktrees
|
|
614
|
+
node bin/hooks/worktree-service.js status # Show current status
|
|
615
|
+
node bin/hooks/worktree-service.js cleanup # Remove stale worktrees
|
|
616
|
+
node bin/hooks/worktree-service.js prune # Run git prune
|
|
617
|
+
node bin/hooks/worktree-service.js remove <name> # Remove specific worktree
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Environment Variables
|
|
621
|
+
|
|
622
|
+
| Variable | Default | Description |
|
|
623
|
+
|-----------|----------|-------------|
|
|
624
|
+
| `AGENTFUL_WORKTREE_MODE` | `auto` | Worktree enforcement mode |
|
|
625
|
+
| `AGENTFUL_WORKTREE_DIR` | (auto-set) | Current worktree path |
|
|
626
|
+
| `AGENTFUL_WORKTREE_LOCATION` | `../` | Where to create worktrees |
|
|
627
|
+
| `AGENTFUL_WORKTREE_AUTO_CLEANUP` | `true` | Auto-remove after completion |
|
|
628
|
+
| `AGENTFUL_WORKTREE_RETENTION_DAYS` | `7` | Days before stale cleanup |
|
|
629
|
+
| `AGENTFUL_WORKTREE_MAX_ACTIVE` | `5` | Maximum active worktrees |
|
|
630
|
+
|
|
631
|
+
### CI/CD Detection
|
|
632
|
+
|
|
633
|
+
Worktree mode auto-disables in CI environments:
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
if process.env.CI == "true" or process.env.GITHUB_ACTIONS == "true":
|
|
637
|
+
# Skip worktree creation
|
|
638
|
+
# CI already provides isolated environments
|
|
639
|
+
```
|
|
640
|
+
|
|
459
641
|
## Delegation Pattern
|
|
460
642
|
|
|
461
643
|
**NEVER implement yourself.** Always use Task tool.
|
|
@@ -38,6 +38,24 @@ Fall back to manual Grep if none available
|
|
|
38
38
|
|
|
39
39
|
**Reference the validation skill** (`.claude/skills/validation/SKILL.md`) for comprehensive validation strategies.
|
|
40
40
|
|
|
41
|
+
## Step 1.5: Worktree Check
|
|
42
|
+
|
|
43
|
+
Before running checks, verify your working environment:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Check if AGENTFUL_WORKTREE_DIR is set
|
|
47
|
+
if exists("$AGENTFUL_WORKTREE_DIR"):
|
|
48
|
+
worktree_path = "$AGENTFUL_WORKTREE_DIR"
|
|
49
|
+
echo "✅ Reviewing in worktree: $worktree_path"
|
|
50
|
+
else:
|
|
51
|
+
echo "📍 Reviewing in root repository"
|
|
52
|
+
echo "⚠️ Validation reports will be saved to main .agentful/"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Report worktree status**: In your validation report, always include:
|
|
56
|
+
- Worktree path (if applicable)
|
|
57
|
+
- Branch being validated
|
|
58
|
+
|
|
41
59
|
## Your Scope
|
|
42
60
|
|
|
43
61
|
- **Type Checking** - Run type checker (tsc, mypy, etc.)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentful-worktree
|
|
3
|
+
description: Manage git worktrees for parallel agent development. List, create, and cleanup worktrees.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /agentful-worktree
|
|
7
|
+
|
|
8
|
+
Manage git worktrees for parallel agentful development.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
/agentful-worktree # List all active worktrees
|
|
14
|
+
/agentful-worktree --status # Show current session worktree
|
|
15
|
+
/agentful-worktree --create feature/x # Create new worktree for branch
|
|
16
|
+
/agentful-worktree --cleanup # Remove stale worktrees
|
|
17
|
+
/agentful-worktree --prune # Run git worktree prune
|
|
18
|
+
/agentful-worktree --remove <name> # Remove specific worktree
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Environment Variables
|
|
22
|
+
|
|
23
|
+
| Variable | Default | Description |
|
|
24
|
+
|-----------|----------|-------------|
|
|
25
|
+
| `AGENTFUL_WORKTREE_MODE` | `auto` | `auto` (create), `block` (require), `off` (disabled) |
|
|
26
|
+
| `AGENTFUL_WORKTREE_LOCATION` | `../` | Where to create worktrees (relative to repo) |
|
|
27
|
+
| `AGENTFUL_WORKTREE_AUTO_CLEANUP` | `true` | Auto-remove worktrees after task completion |
|
|
28
|
+
| `AGENTFUL_WORKTREE_RETENTION_DAYS` | `7` | Days before worktrees are marked stale |
|
|
29
|
+
| `AGENTFUL_WORKTREE_MAX_ACTIVE` | `5` | Maximum active worktrees before cleanup forced |
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### List Worktrees
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
/agentful-worktree
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
```
|
|
41
|
+
📋 Active Worktrees:
|
|
42
|
+
|
|
43
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
44
|
+
│ agentful-feature-auth-1739297120 │
|
|
45
|
+
│ ├─ Branch: feature/auth │
|
|
46
|
+
│ ├─ Purpose: feature │
|
|
47
|
+
│ ├─ Path: ../agentful-feature-auth-1739297120 │
|
|
48
|
+
│ ├─ Status: 🟢 Active (15 min ago) │
|
|
49
|
+
│ └─ Created: 2025-02-11T15:30:00.000Z │
|
|
50
|
+
└─────────────────────────────────────────────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Show Current Status
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
/agentful-worktree --status
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Create Worktree
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
/agentful-worktree --create feature/dashboard
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Creates a worktree named `agentful-feature-dashboard-<timestamp>` on the `feature/dashboard` branch.
|
|
66
|
+
|
|
67
|
+
### Cleanup Stale Worktrees
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/agentful-worktree --cleanup
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Removes worktrees older than `AGENTFUL_WORKTREE_RETENTION_DAYS` (default: 7 days).
|
|
74
|
+
|
|
75
|
+
## Worktree Naming Convention
|
|
76
|
+
|
|
77
|
+
Worktrees are automatically named using this pattern:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
agentful-<purpose>-<branch-slug>-<timestamp>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
- `agentful-feature-auth-1739297120` - Feature development
|
|
85
|
+
- `agentful-fix-coverage-1739297150` - Fixer adding coverage
|
|
86
|
+
- `agentful-review-1739297180` - Reviewer validating
|
|
87
|
+
- `agentful-hotfix-login-bug-1739297210` - Hotfix work
|
|
88
|
+
|
|
89
|
+
## When to Use Worktrees
|
|
90
|
+
|
|
91
|
+
**Use worktrees when**:
|
|
92
|
+
- Working on multiple features simultaneously
|
|
93
|
+
- Running quality gates while developing other features
|
|
94
|
+
- Reviewing PRs without interrupting active work
|
|
95
|
+
- Testing experimental approaches safely
|
|
96
|
+
|
|
97
|
+
**Don't need worktrees when**:
|
|
98
|
+
- Quick fixes in a single feature branch
|
|
99
|
+
- Documentation-only changes
|
|
100
|
+
- Set `AGENTFUL_WORKTREE_MODE=off` to disable
|
|
101
|
+
|
|
102
|
+
## See Also
|
|
103
|
+
|
|
104
|
+
- [Git Worktrees Concept](/concepts/git-worktrees) - Deep dive on worktree usage
|
|
105
|
+
- [/agentful-start](/commands/agentful-start) - Main development loop
|
|
106
|
+
- [/agentful-status](/commands/agentful-status) - Check completion progress
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"includeCoAuthoredBy": false,
|
|
3
3
|
"env": {
|
|
4
|
-
"ENABLE_TOOL_SEARCH": "true"
|
|
4
|
+
"ENABLE_TOOL_SEARCH": "true",
|
|
5
|
+
"AGENTFUL_WORKTREE_MODE": "auto"
|
|
5
6
|
},
|
|
6
7
|
"hooks": {
|
|
7
8
|
"SessionStart": [
|
|
@@ -23,6 +24,17 @@
|
|
|
23
24
|
}
|
|
24
25
|
],
|
|
25
26
|
"PreToolUse": [
|
|
27
|
+
{
|
|
28
|
+
"tools": ["Write", "Edit"],
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node bin/hooks/ensure-worktree.js",
|
|
33
|
+
"timeout": 10,
|
|
34
|
+
"description": "Ensure agents work in git worktrees (disable with AGENTFUL_WORKTREE_MODE=off)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
26
38
|
{
|
|
27
39
|
"tools": ["Write", "Edit"],
|
|
28
40
|
"hooks": [
|
package/template/CLAUDE.md
CHANGED
|
@@ -199,6 +199,33 @@ The `reviewer` agent runs these checks automatically. The `fixer` agent resolves
|
|
|
199
199
|
- Estimated remaining work
|
|
200
200
|
→ If no progress for 2+ minutes, the task may be stuck. Check `.agentful/state.json` for circuit breaker status.
|
|
201
201
|
|
|
202
|
+
### Git Worktree Mode
|
|
203
|
+
|
|
204
|
+
agentful supports automatic git worktree management for safer parallel development.
|
|
205
|
+
|
|
206
|
+
**Modes**:
|
|
207
|
+
| Mode | Behavior |
|
|
208
|
+
|-------|-----------|
|
|
209
|
+
| `auto` | Create worktrees automatically when agents make changes (recommended) |
|
|
210
|
+
| `block` | Require agents to work in existing worktrees |
|
|
211
|
+
| `off` | Allow direct edits to root repository (legacy) |
|
|
212
|
+
|
|
213
|
+
**Enable via environment**:
|
|
214
|
+
```bash
|
|
215
|
+
export AGENTFUL_WORKTREE_MODE=auto
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Or in `.claude/settings.json`**:
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"env": {
|
|
222
|
+
"AGENTFUL_WORKTREE_MODE": "auto"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**More configuration**: See [/agentful-worktree](/commands/agentful-worktree) command and [Git Worktrees concept](/concepts/git-worktrees) for full details.
|
|
228
|
+
|
|
202
229
|
## Getting Help
|
|
203
230
|
|
|
204
231
|
**Documentation**: See `.claude/commands/` for detailed command documentation
|
package/version.json
CHANGED