@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.
Files changed (56) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/parallel.d.ts +7 -0
  3. package/dist/commands/parallel.d.ts.map +1 -0
  4. package/dist/commands/parallel.js +238 -0
  5. package/dist/commands/session.d.ts +7 -0
  6. package/dist/commands/session.d.ts.map +1 -0
  7. package/dist/commands/specs.d.ts +6 -0
  8. package/dist/commands/specs.d.ts.map +1 -1
  9. package/dist/commands/specs.js +55 -2
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +13 -3
  12. package/dist/commands/tutorial.js +0 -2
  13. package/dist/commands/waivers.d.ts.map +1 -1
  14. package/dist/constants/spec-types.d.ts +52 -0
  15. package/dist/constants/spec-types.d.ts.map +1 -1
  16. package/dist/constants/spec-types.js +25 -2
  17. package/dist/index.js +43 -2
  18. package/dist/parallel/parallel-manager.d.ts +67 -0
  19. package/dist/parallel/parallel-manager.d.ts.map +1 -0
  20. package/dist/parallel/parallel-manager.js +440 -0
  21. package/dist/scaffold/claude-hooks.d.ts.map +1 -1
  22. package/dist/scaffold/claude-hooks.js +78 -2
  23. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  24. package/dist/scaffold/git-hooks.js +237 -73
  25. package/dist/scaffold/index.d.ts.map +1 -1
  26. package/dist/session/session-manager.d.ts +94 -0
  27. package/dist/session/session-manager.d.ts.map +1 -0
  28. package/dist/session/session-manager.js +14 -0
  29. package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
  30. package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
  31. package/dist/templates/.claude/hooks/session-log.sh +528 -0
  32. package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  33. package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
  34. package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  35. package/dist/templates/.claude/rules/git-safety.md +26 -0
  36. package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
  37. package/dist/templates/.claude/settings.json +15 -0
  38. package/dist/utils/gitignore-updater.d.ts +1 -1
  39. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  40. package/dist/utils/gitignore-updater.js +3 -0
  41. package/dist/utils/ide-detection.d.ts +89 -0
  42. package/dist/utils/ide-detection.d.ts.map +1 -0
  43. package/dist/validation/spec-validation.d.ts.map +1 -1
  44. package/dist/validation/spec-validation.js +16 -0
  45. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  46. package/dist/worktree/worktree-manager.js +31 -21
  47. package/package.json +2 -2
  48. package/templates/.claude/hooks/scope-guard.sh +67 -21
  49. package/templates/.claude/hooks/session-caws-status.sh +117 -0
  50. package/templates/.claude/hooks/session-log.sh +528 -0
  51. package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  52. package/templates/.claude/hooks/worktree-guard.sh +207 -0
  53. package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  54. package/templates/.claude/rules/git-safety.md +26 -0
  55. package/templates/.claude/rules/worktree-isolation.md +51 -0
  56. 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,iBA6HlB;AAED;;;;;GAKG;AACH,+CAJW,MAAM,EAAE,2BAuJlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAcnB;AAED;;;;GAIG;AACH,iDAHW,MAAM,OAehB"}
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;AA4kBD;;;GAGG;AACH,2CAFW,MAAM,iBAkChB;AAED;;;GAGG;AACH,gDAFW,MAAM,iBAgDhB;AA9VD;;;GAGG;AACH,8CAqNC;AA7hBD;;;GAGG;AACH,4DAsRC;AAED;;GAEG;AACH,iDAmCC;AA6ND;;GAEG;AACH,gDAsCC"}
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"}