@paths.design/caws-cli 8.3.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/parallel.d.ts +7 -0
- package/dist/commands/parallel.d.ts.map +1 -0
- package/dist/commands/parallel.js +238 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/specs.d.ts +6 -0
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +55 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -3
- package/dist/commands/tutorial.js +0 -2
- package/dist/commands/waivers.d.ts.map +1 -1
- package/dist/constants/spec-types.d.ts +52 -0
- package/dist/constants/spec-types.d.ts.map +1 -1
- package/dist/constants/spec-types.js +25 -2
- package/dist/index.js +43 -2
- package/dist/parallel/parallel-manager.d.ts +67 -0
- package/dist/parallel/parallel-manager.d.ts.map +1 -0
- package/dist/parallel/parallel-manager.js +440 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -1
- package/dist/scaffold/claude-hooks.js +78 -2
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +237 -73
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +94 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +14 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
- package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/dist/templates/.claude/hooks/session-log.sh +528 -0
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/dist/templates/.claude/rules/git-safety.md +26 -0
- package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
- package/dist/templates/.claude/settings.json +15 -0
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/ide-detection.d.ts +89 -0
- package/dist/utils/ide-detection.d.ts.map +1 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +16 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.js +31 -21
- package/package.json +2 -2
- package/templates/.claude/hooks/scope-guard.sh +67 -21
- package/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/templates/.claude/hooks/session-log.sh +528 -0
- package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/templates/.claude/rules/git-safety.md +26 -0
- package/templates/.claude/rules/worktree-isolation.md +51 -0
- package/templates/.claude/settings.json +15 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load and validate a parallel plan YAML file
|
|
3
|
+
* @param {string} filePath - Path to plan YAML file
|
|
4
|
+
* @returns {Object} Parsed and validated plan
|
|
5
|
+
*/
|
|
6
|
+
export function loadPlan(filePath: string): any;
|
|
7
|
+
/**
|
|
8
|
+
* Set up parallel worktrees from a plan
|
|
9
|
+
* @param {Object} plan - Validated plan from loadPlan
|
|
10
|
+
* @returns {Object[]} Array of created worktree entries
|
|
11
|
+
*/
|
|
12
|
+
export function setupParallel(plan: any): any[];
|
|
13
|
+
/**
|
|
14
|
+
* Get status of all parallel worktrees
|
|
15
|
+
* @returns {Object|null} Parallel status or null if no active run
|
|
16
|
+
*/
|
|
17
|
+
export function getParallelStatus(): any | null;
|
|
18
|
+
/**
|
|
19
|
+
* Merge all parallel branches back to base
|
|
20
|
+
* @param {Object} options - Merge options
|
|
21
|
+
* @param {string} [options.strategy] - Override merge strategy
|
|
22
|
+
* @param {boolean} [options.dryRun] - Preview without executing
|
|
23
|
+
* @param {boolean} [options.force] - Force merge even with conflicts
|
|
24
|
+
* @returns {Object} Merge results {merged, failed, conflicts}
|
|
25
|
+
*/
|
|
26
|
+
export function mergeParallel(options?: {
|
|
27
|
+
strategy?: string;
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}): any;
|
|
31
|
+
/**
|
|
32
|
+
* Tear down all parallel worktrees
|
|
33
|
+
* @param {Object} options - Teardown options
|
|
34
|
+
* @param {boolean} [options.deleteBranches] - Also delete branches
|
|
35
|
+
* @param {boolean} [options.force] - Force removal even if dirty
|
|
36
|
+
* @returns {Object} Teardown results {destroyed, failed}
|
|
37
|
+
*/
|
|
38
|
+
export function teardownParallel(options?: {
|
|
39
|
+
deleteBranches?: boolean;
|
|
40
|
+
force?: boolean;
|
|
41
|
+
}): any;
|
|
42
|
+
/**
|
|
43
|
+
* Detect file-level conflicts between agent branches
|
|
44
|
+
* @param {string} baseBranch - Base branch name
|
|
45
|
+
* @param {Object[]} agentStatuses - Agent status objects with branch field
|
|
46
|
+
* @returns {Object[]} Conflicts: [{file, agents: [name, name]}]
|
|
47
|
+
*/
|
|
48
|
+
export function detectFileConflicts(baseBranch: string, agentStatuses: any[]): any[];
|
|
49
|
+
/**
|
|
50
|
+
* Load the parallel registry
|
|
51
|
+
* @param {string} root - Repository root
|
|
52
|
+
* @returns {Object|null} Registry or null if not found
|
|
53
|
+
*/
|
|
54
|
+
export function loadParallelRegistry(root: string): any | null;
|
|
55
|
+
/**
|
|
56
|
+
* Save the parallel registry
|
|
57
|
+
* @param {string} root - Repository root
|
|
58
|
+
* @param {Object} data - Registry data
|
|
59
|
+
*/
|
|
60
|
+
export function saveParallelRegistry(root: string, data: any): void;
|
|
61
|
+
/**
|
|
62
|
+
* Remove the parallel registry
|
|
63
|
+
* @param {string} root - Repository root
|
|
64
|
+
*/
|
|
65
|
+
export function removeParallelRegistry(root: string): void;
|
|
66
|
+
export const PARALLEL_REGISTRY: ".caws/parallel.json";
|
|
67
|
+
//# sourceMappingURL=parallel-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parallel-manager.d.ts","sourceRoot":"","sources":["../../src/parallel/parallel-manager.js"],"names":[],"mappings":"AA6EA;;;;GAIG;AACH,mCAHW,MAAM,OAoDhB;AAED;;;;GAIG;AACH,0CAFa,KAAQ,CAyCpB;AAED;;;GAGG;AACH,qCAFa,MAAO,IAAI,CA2DvB;AA2CD;;;;;;;GAOG;AACH,wCALG;IAAyB,QAAQ,GAAzB,MAAM;IACY,MAAM,GAAxB,OAAO;IACW,KAAK,GAAvB,OAAO;CACf,OA2GF;AAED;;;;;;GAMG;AACH,2CAJG;IAA0B,cAAc,GAAhC,OAAO;IACW,KAAK,GAAvB,OAAO;CACf,OA0BF;AA3LD;;;;;GAKG;AACH,gDAJW,MAAM,iBACN,KAAQ,GACN,KAAQ,CAmCpB;AAnPD;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAO,IAAI,CAYvB;AAED;;;;GAIG;AACH,2CAHW,MAAM,mBAOhB;AAED;;;GAGG;AACH,6CAFW,MAAM,QAOhB;AAnDD,gCAA0B,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Parallel Workspace Manager
|
|
3
|
+
* Orchestrates multi-agent worktree + session setup from a plan file.
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const yaml = require('js-yaml');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
createWorktree,
|
|
14
|
+
listWorktrees,
|
|
15
|
+
destroyWorktree,
|
|
16
|
+
getRepoRoot,
|
|
17
|
+
BRANCH_PREFIX,
|
|
18
|
+
} = require('../worktree/worktree-manager');
|
|
19
|
+
|
|
20
|
+
// session-manager available if needed: require('../session/session-manager')
|
|
21
|
+
|
|
22
|
+
const PARALLEL_REGISTRY = '.caws/parallel.json';
|
|
23
|
+
const VALID_STRATEGIES = ['merge', 'rebase', 'squash'];
|
|
24
|
+
const NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get current branch name
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function getCurrentBranch() {
|
|
31
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the parallel registry
|
|
38
|
+
* @param {string} root - Repository root
|
|
39
|
+
* @returns {Object|null} Registry or null if not found
|
|
40
|
+
*/
|
|
41
|
+
function loadParallelRegistry(root) {
|
|
42
|
+
const regPath = path.join(root, PARALLEL_REGISTRY);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(regPath)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(regPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Corrupted registry
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save the parallel registry
|
|
55
|
+
* @param {string} root - Repository root
|
|
56
|
+
* @param {Object} data - Registry data
|
|
57
|
+
*/
|
|
58
|
+
function saveParallelRegistry(root, data) {
|
|
59
|
+
const regPath = path.join(root, PARALLEL_REGISTRY);
|
|
60
|
+
fs.ensureDirSync(path.dirname(regPath));
|
|
61
|
+
fs.writeFileSync(regPath, JSON.stringify(data, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Remove the parallel registry
|
|
66
|
+
* @param {string} root - Repository root
|
|
67
|
+
*/
|
|
68
|
+
function removeParallelRegistry(root) {
|
|
69
|
+
const regPath = path.join(root, PARALLEL_REGISTRY);
|
|
70
|
+
if (fs.existsSync(regPath)) {
|
|
71
|
+
fs.removeSync(regPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load and validate a parallel plan YAML file
|
|
77
|
+
* @param {string} filePath - Path to plan YAML file
|
|
78
|
+
* @returns {Object} Parsed and validated plan
|
|
79
|
+
*/
|
|
80
|
+
function loadPlan(filePath) {
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
82
|
+
throw new Error(`Plan file not found: ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
86
|
+
let plan;
|
|
87
|
+
try {
|
|
88
|
+
plan = yaml.load(content);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new Error(`Invalid YAML in plan file: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!plan || typeof plan !== 'object') {
|
|
94
|
+
throw new Error('Plan file must contain a YAML object');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (plan.version !== 1) {
|
|
98
|
+
throw new Error(`Unsupported plan version: ${plan.version}. Expected 1.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!Array.isArray(plan.agents) || plan.agents.length === 0) {
|
|
102
|
+
throw new Error('Plan must define at least one agent');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (plan.merge_strategy && !VALID_STRATEGIES.includes(plan.merge_strategy)) {
|
|
106
|
+
throw new Error(`Invalid merge_strategy: ${plan.merge_strategy}. Must be one of: ${VALID_STRATEGIES.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const names = new Set();
|
|
110
|
+
for (const agent of plan.agents) {
|
|
111
|
+
if (!agent.name) {
|
|
112
|
+
throw new Error('Each agent must have a name');
|
|
113
|
+
}
|
|
114
|
+
if (!NAME_REGEX.test(agent.name)) {
|
|
115
|
+
throw new Error(`Invalid agent name '${agent.name}': must contain only letters, numbers, hyphens, and underscores`);
|
|
116
|
+
}
|
|
117
|
+
if (names.has(agent.name)) {
|
|
118
|
+
throw new Error(`Duplicate agent name: ${agent.name}`);
|
|
119
|
+
}
|
|
120
|
+
names.add(agent.name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
version: plan.version,
|
|
125
|
+
baseBranch: plan.base_branch || null,
|
|
126
|
+
mergeStrategy: plan.merge_strategy || 'merge',
|
|
127
|
+
agents: plan.agents,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Set up parallel worktrees from a plan
|
|
133
|
+
* @param {Object} plan - Validated plan from loadPlan
|
|
134
|
+
* @returns {Object[]} Array of created worktree entries
|
|
135
|
+
*/
|
|
136
|
+
function setupParallel(plan) {
|
|
137
|
+
const root = getRepoRoot();
|
|
138
|
+
|
|
139
|
+
// Check for existing parallel run
|
|
140
|
+
const existing = loadParallelRegistry(root);
|
|
141
|
+
if (existing && existing.agents && existing.agents.length > 0) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
'A parallel run is already active. Run `caws parallel teardown` first, or `caws parallel status` to inspect.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const baseBranch = plan.baseBranch || getCurrentBranch();
|
|
148
|
+
const results = [];
|
|
149
|
+
|
|
150
|
+
for (const agent of plan.agents) {
|
|
151
|
+
const entry = createWorktree(agent.name, {
|
|
152
|
+
scope: agent.scope || null,
|
|
153
|
+
baseBranch,
|
|
154
|
+
specId: agent.spec_id || null,
|
|
155
|
+
});
|
|
156
|
+
results.push({ ...entry, intent: agent.intent || null, role: agent.role || 'worker' });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Write parallel registry
|
|
160
|
+
saveParallelRegistry(root, {
|
|
161
|
+
version: 1,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
baseBranch,
|
|
164
|
+
mergeStrategy: plan.mergeStrategy || 'merge',
|
|
165
|
+
agents: plan.agents.map((a) => ({
|
|
166
|
+
name: a.name,
|
|
167
|
+
scope: a.scope || null,
|
|
168
|
+
specId: a.spec_id || null,
|
|
169
|
+
role: a.role || 'worker',
|
|
170
|
+
intent: a.intent || null,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get status of all parallel worktrees
|
|
179
|
+
* @returns {Object|null} Parallel status or null if no active run
|
|
180
|
+
*/
|
|
181
|
+
function getParallelStatus() {
|
|
182
|
+
const root = getRepoRoot();
|
|
183
|
+
const parallelReg = loadParallelRegistry(root);
|
|
184
|
+
if (!parallelReg) return null;
|
|
185
|
+
|
|
186
|
+
const worktrees = listWorktrees();
|
|
187
|
+
|
|
188
|
+
const agentStatuses = parallelReg.agents.map((agent) => {
|
|
189
|
+
const wt = worktrees.find((w) => w.name === agent.name);
|
|
190
|
+
let commitCount = 0;
|
|
191
|
+
let dirty = false;
|
|
192
|
+
|
|
193
|
+
if (wt && wt.status === 'active') {
|
|
194
|
+
try {
|
|
195
|
+
const log = execFileSync(
|
|
196
|
+
'git',
|
|
197
|
+
['log', '--oneline', `${parallelReg.baseBranch}..${wt.branch}`],
|
|
198
|
+
{ cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
199
|
+
);
|
|
200
|
+
commitCount = log.trim().split('\n').filter(Boolean).length;
|
|
201
|
+
} catch {
|
|
202
|
+
// Branch may not have diverged
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
207
|
+
cwd: wt.path,
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
210
|
+
});
|
|
211
|
+
dirty = status.trim().length > 0;
|
|
212
|
+
} catch {
|
|
213
|
+
// Worktree may be inaccessible
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: agent.name,
|
|
219
|
+
branch: wt ? wt.branch : BRANCH_PREFIX + agent.name,
|
|
220
|
+
status: wt ? wt.status : 'missing',
|
|
221
|
+
scope: agent.scope || '(all)',
|
|
222
|
+
commitCount,
|
|
223
|
+
dirty,
|
|
224
|
+
intent: agent.intent,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Detect file-level conflicts between agents
|
|
229
|
+
const conflicts = detectFileConflicts(parallelReg.baseBranch, agentStatuses);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
baseBranch: parallelReg.baseBranch,
|
|
233
|
+
mergeStrategy: parallelReg.mergeStrategy,
|
|
234
|
+
createdAt: parallelReg.createdAt,
|
|
235
|
+
agents: agentStatuses,
|
|
236
|
+
conflicts,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Detect file-level conflicts between agent branches
|
|
242
|
+
* @param {string} baseBranch - Base branch name
|
|
243
|
+
* @param {Object[]} agentStatuses - Agent status objects with branch field
|
|
244
|
+
* @returns {Object[]} Conflicts: [{file, agents: [name, name]}]
|
|
245
|
+
*/
|
|
246
|
+
function detectFileConflicts(baseBranch, agentStatuses) {
|
|
247
|
+
const root = getRepoRoot();
|
|
248
|
+
const filesByAgent = {};
|
|
249
|
+
|
|
250
|
+
for (const agent of agentStatuses) {
|
|
251
|
+
if (agent.status !== 'active') continue;
|
|
252
|
+
try {
|
|
253
|
+
const diff = execFileSync(
|
|
254
|
+
'git',
|
|
255
|
+
['diff', '--name-only', `${baseBranch}...${agent.branch}`],
|
|
256
|
+
{ cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
257
|
+
);
|
|
258
|
+
filesByAgent[agent.name] = diff.trim().split('\n').filter(Boolean);
|
|
259
|
+
} catch {
|
|
260
|
+
filesByAgent[agent.name] = [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Find overlapping files
|
|
265
|
+
const conflicts = [];
|
|
266
|
+
const agentNames = Object.keys(filesByAgent);
|
|
267
|
+
for (let i = 0; i < agentNames.length; i++) {
|
|
268
|
+
for (let j = i + 1; j < agentNames.length; j++) {
|
|
269
|
+
const a = agentNames[i];
|
|
270
|
+
const b = agentNames[j];
|
|
271
|
+
const overlap = filesByAgent[a].filter((f) => filesByAgent[b].includes(f));
|
|
272
|
+
for (const file of overlap) {
|
|
273
|
+
conflicts.push({ file, agents: [a, b] });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return conflicts;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Merge all parallel branches back to base
|
|
283
|
+
* @param {Object} options - Merge options
|
|
284
|
+
* @param {string} [options.strategy] - Override merge strategy
|
|
285
|
+
* @param {boolean} [options.dryRun] - Preview without executing
|
|
286
|
+
* @param {boolean} [options.force] - Force merge even with conflicts
|
|
287
|
+
* @returns {Object} Merge results {merged, failed, conflicts}
|
|
288
|
+
*/
|
|
289
|
+
function mergeParallel(options = {}) {
|
|
290
|
+
const root = getRepoRoot();
|
|
291
|
+
const parallelReg = loadParallelRegistry(root);
|
|
292
|
+
if (!parallelReg) {
|
|
293
|
+
throw new Error('No active parallel run found. Run `caws parallel setup` first.');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const strategy = options.strategy || parallelReg.mergeStrategy || 'merge';
|
|
297
|
+
const worktrees = listWorktrees();
|
|
298
|
+
const activeAgents = parallelReg.agents
|
|
299
|
+
.map((a) => {
|
|
300
|
+
const wt = worktrees.find((w) => w.name === a.name);
|
|
301
|
+
return wt ? { ...a, ...wt } : null;
|
|
302
|
+
})
|
|
303
|
+
.filter((a) => a && a.status === 'active');
|
|
304
|
+
|
|
305
|
+
// Check for dirty worktrees
|
|
306
|
+
for (const agent of activeAgents) {
|
|
307
|
+
try {
|
|
308
|
+
const status = execFileSync('git', ['status', '--porcelain'], {
|
|
309
|
+
cwd: agent.path,
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
312
|
+
});
|
|
313
|
+
if (status.trim().length > 0) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Worktree '${agent.name}' has uncommitted changes. Commit or stash before merging.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err.message.includes('uncommitted changes')) throw err;
|
|
320
|
+
// Worktree may be inaccessible
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Build agent status for conflict detection
|
|
325
|
+
const agentStatuses = activeAgents.map((a) => ({
|
|
326
|
+
name: a.name,
|
|
327
|
+
branch: a.branch,
|
|
328
|
+
status: 'active',
|
|
329
|
+
}));
|
|
330
|
+
const conflicts = detectFileConflicts(parallelReg.baseBranch, agentStatuses);
|
|
331
|
+
|
|
332
|
+
if (conflicts.length > 0 && !options.force) {
|
|
333
|
+
return { merged: [], failed: [], conflicts };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (options.dryRun) {
|
|
337
|
+
return {
|
|
338
|
+
merged: activeAgents.map((a) => a.name),
|
|
339
|
+
failed: [],
|
|
340
|
+
conflicts,
|
|
341
|
+
dryRun: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Checkout base branch
|
|
346
|
+
execFileSync('git', ['checkout', parallelReg.baseBranch], {
|
|
347
|
+
cwd: root,
|
|
348
|
+
stdio: 'pipe',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const merged = [];
|
|
352
|
+
const failed = [];
|
|
353
|
+
|
|
354
|
+
for (const agent of activeAgents) {
|
|
355
|
+
try {
|
|
356
|
+
if (strategy === 'rebase') {
|
|
357
|
+
execFileSync('git', ['rebase', agent.branch], {
|
|
358
|
+
cwd: root,
|
|
359
|
+
stdio: 'pipe',
|
|
360
|
+
});
|
|
361
|
+
} else if (strategy === 'squash') {
|
|
362
|
+
execFileSync('git', ['merge', '--squash', agent.branch], {
|
|
363
|
+
cwd: root,
|
|
364
|
+
stdio: 'pipe',
|
|
365
|
+
});
|
|
366
|
+
execFileSync(
|
|
367
|
+
'git',
|
|
368
|
+
['commit', '-m', `feat: merge ${agent.name} (squashed)`],
|
|
369
|
+
{ cwd: root, stdio: 'pipe' }
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
execFileSync('git', ['merge', agent.branch, '--no-edit'], {
|
|
373
|
+
cwd: root,
|
|
374
|
+
stdio: 'pipe',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
merged.push(agent.name);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
// Abort failed merge
|
|
380
|
+
try {
|
|
381
|
+
execFileSync('git', ['merge', '--abort'], { cwd: root, stdio: 'pipe' });
|
|
382
|
+
} catch {
|
|
383
|
+
try {
|
|
384
|
+
execFileSync('git', ['rebase', '--abort'], { cwd: root, stdio: 'pipe' });
|
|
385
|
+
} catch {
|
|
386
|
+
// Already clean
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
failed.push({ name: agent.name, error: err.message });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { merged, failed, conflicts };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Tear down all parallel worktrees
|
|
398
|
+
* @param {Object} options - Teardown options
|
|
399
|
+
* @param {boolean} [options.deleteBranches] - Also delete branches
|
|
400
|
+
* @param {boolean} [options.force] - Force removal even if dirty
|
|
401
|
+
* @returns {Object} Teardown results {destroyed, failed}
|
|
402
|
+
*/
|
|
403
|
+
function teardownParallel(options = {}) {
|
|
404
|
+
const root = getRepoRoot();
|
|
405
|
+
const parallelReg = loadParallelRegistry(root);
|
|
406
|
+
if (!parallelReg) {
|
|
407
|
+
throw new Error('No active parallel run found.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const { deleteBranches = false, force = false } = options;
|
|
411
|
+
const destroyed = [];
|
|
412
|
+
const failed = [];
|
|
413
|
+
|
|
414
|
+
for (const agent of parallelReg.agents) {
|
|
415
|
+
try {
|
|
416
|
+
destroyWorktree(agent.name, { deleteBranch: deleteBranches, force });
|
|
417
|
+
destroyed.push(agent.name);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
failed.push({ name: agent.name, error: err.message });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Remove parallel registry
|
|
424
|
+
removeParallelRegistry(root);
|
|
425
|
+
|
|
426
|
+
return { destroyed, failed };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
loadPlan,
|
|
431
|
+
setupParallel,
|
|
432
|
+
getParallelStatus,
|
|
433
|
+
mergeParallel,
|
|
434
|
+
teardownParallel,
|
|
435
|
+
detectFileConflicts,
|
|
436
|
+
loadParallelRegistry,
|
|
437
|
+
saveParallelRegistry,
|
|
438
|
+
removeParallelRegistry,
|
|
439
|
+
PARALLEL_REGISTRY,
|
|
440
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/claude-hooks.js"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,gDAHW,MAAM,WACN,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"claude-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/claude-hooks.js"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,gDAHW,MAAM,WACN,MAAM,EAAE,iBA0IlB;AAED;;;;;GAKG;AACH,+CAJW,MAAM,EAAE,2BAsNlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAcnB;AAED;;;;GAIG;AACH,iDAHW,MAAM,OAehB"}
|
|
@@ -53,10 +53,10 @@ async function scaffoldClaudeHooks(projectDir, levels = ['safety', 'quality', 's
|
|
|
53
53
|
|
|
54
54
|
// Map levels to hook scripts
|
|
55
55
|
const hookMapping = {
|
|
56
|
-
safety: ['block-dangerous.sh', 'scan-secrets.sh'],
|
|
56
|
+
safety: ['block-dangerous.sh', 'scan-secrets.sh', 'worktree-guard.sh', 'worktree-write-guard.sh', 'stop-worktree-check.sh', 'session-caws-status.sh'],
|
|
57
57
|
quality: ['quality-check.sh', 'validate-spec.sh'],
|
|
58
58
|
scope: ['scope-guard.sh', 'naming-check.sh'],
|
|
59
|
-
audit: ['audit.sh'],
|
|
59
|
+
audit: ['audit.sh', 'session-log.sh'],
|
|
60
60
|
lite: ['block-dangerous.sh', 'scope-guard.sh', 'lite-sprawl-check.sh', 'simplification-guard.sh'],
|
|
61
61
|
};
|
|
62
62
|
|
|
@@ -83,6 +83,11 @@ async function scaffoldClaudeHooks(projectDir, levels = ['safety', 'quality', 's
|
|
|
83
83
|
'naming-check.sh',
|
|
84
84
|
'lite-sprawl-check.sh',
|
|
85
85
|
'simplification-guard.sh',
|
|
86
|
+
'worktree-guard.sh',
|
|
87
|
+
'worktree-write-guard.sh',
|
|
88
|
+
'stop-worktree-check.sh',
|
|
89
|
+
'session-caws-status.sh',
|
|
90
|
+
'session-log.sh',
|
|
86
91
|
];
|
|
87
92
|
|
|
88
93
|
for (const script of allHookScripts) {
|
|
@@ -131,6 +136,14 @@ async function scaffoldClaudeHooks(projectDir, levels = ['safety', 'quality', 's
|
|
|
131
136
|
await fs.copy(readmePath, path.join(claudeDir, 'README.md'));
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
// Copy rules directory if it exists
|
|
140
|
+
const rulesTemplateDir = path.join(claudeTemplateDir, 'rules');
|
|
141
|
+
if (fs.existsSync(rulesTemplateDir)) {
|
|
142
|
+
const rulesDir = path.join(claudeDir, 'rules');
|
|
143
|
+
await fs.ensureDir(rulesDir);
|
|
144
|
+
await fs.copy(rulesTemplateDir, rulesDir, { overwrite: false });
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
console.log(chalk.green('Claude Code hooks configured'));
|
|
135
148
|
console.log(chalk.gray(` Enabled: ${levels.join(', ')}`));
|
|
136
149
|
console.log(
|
|
@@ -168,6 +181,11 @@ function generateClaudeSettings(levels, _enabledHooks) {
|
|
|
168
181
|
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous.sh',
|
|
169
182
|
timeout: 10,
|
|
170
183
|
},
|
|
184
|
+
{
|
|
185
|
+
type: 'command',
|
|
186
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/worktree-guard.sh',
|
|
187
|
+
timeout: 10,
|
|
188
|
+
},
|
|
171
189
|
],
|
|
172
190
|
});
|
|
173
191
|
|
|
@@ -182,6 +200,42 @@ function generateClaudeSettings(levels, _enabledHooks) {
|
|
|
182
200
|
},
|
|
183
201
|
],
|
|
184
202
|
});
|
|
203
|
+
|
|
204
|
+
// Block Write/Edit on base branch while worktrees are active
|
|
205
|
+
settings.hooks.PreToolUse.push({
|
|
206
|
+
matcher: 'Write|Edit',
|
|
207
|
+
hooks: [
|
|
208
|
+
{
|
|
209
|
+
type: 'command',
|
|
210
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/worktree-write-guard.sh',
|
|
211
|
+
timeout: 10,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Worktree status warning on session start
|
|
217
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
218
|
+
settings.hooks.SessionStart.push({
|
|
219
|
+
hooks: [
|
|
220
|
+
{
|
|
221
|
+
type: 'command',
|
|
222
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-caws-status.sh session-start',
|
|
223
|
+
timeout: 10,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Worktree cleanup reminder on session end
|
|
229
|
+
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
230
|
+
settings.hooks.Stop.push({
|
|
231
|
+
hooks: [
|
|
232
|
+
{
|
|
233
|
+
type: 'command',
|
|
234
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/stop-worktree-check.sh',
|
|
235
|
+
timeout: 10,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
});
|
|
185
239
|
}
|
|
186
240
|
|
|
187
241
|
if (levels.includes('quality')) {
|
|
@@ -267,6 +321,11 @@ function generateClaudeSettings(levels, _enabledHooks) {
|
|
|
267
321
|
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/audit.sh session-start',
|
|
268
322
|
timeout: 5,
|
|
269
323
|
},
|
|
324
|
+
{
|
|
325
|
+
type: 'command',
|
|
326
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-log.sh',
|
|
327
|
+
timeout: 10,
|
|
328
|
+
},
|
|
270
329
|
],
|
|
271
330
|
});
|
|
272
331
|
|
|
@@ -278,6 +337,23 @@ function generateClaudeSettings(levels, _enabledHooks) {
|
|
|
278
337
|
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/audit.sh stop',
|
|
279
338
|
timeout: 5,
|
|
280
339
|
},
|
|
340
|
+
{
|
|
341
|
+
type: 'command',
|
|
342
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-log.sh',
|
|
343
|
+
timeout: 15,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Session transcript generation on context compaction
|
|
349
|
+
settings.hooks.PreCompact = settings.hooks.PreCompact || [];
|
|
350
|
+
settings.hooks.PreCompact.push({
|
|
351
|
+
hooks: [
|
|
352
|
+
{
|
|
353
|
+
type: 'command',
|
|
354
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/session-log.sh',
|
|
355
|
+
timeout: 15,
|
|
356
|
+
},
|
|
281
357
|
],
|
|
282
358
|
});
|
|
283
359
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/git-hooks.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,6CAHW,MAAM;;;GAwGhB;
|
|
1
|
+
{"version":3,"file":"git-hooks.d.ts","sourceRoot":"","sources":["../../src/scaffold/git-hooks.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,6CAHW,MAAM;;;GAwGhB;AAgvBD;;;GAGG;AACH,2CAFW,MAAM,iBAkChB;AAED;;;GAGG;AACH,gDAFW,MAAM,iBAgDhB;AAxYD;;;GAGG;AACH,8CA2MC;AA7oBD;;;GAGG;AACH,4DAgZC;AAED;;GAEG;AACH,iDAmCC;AAmND;;GAEG;AACH,gDA0FC"}
|