@paths.design/caws-cli 8.0.1 → 8.2.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 (112) hide show
  1. package/README.md +5 -6
  2. package/dist/commands/archive.d.ts +1 -0
  3. package/dist/commands/archive.d.ts.map +1 -1
  4. package/dist/commands/archive.js +114 -6
  5. package/dist/commands/burnup.d.ts.map +1 -1
  6. package/dist/commands/burnup.js +109 -10
  7. package/dist/commands/diagnose.js +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +185 -39
  10. package/dist/commands/mode.d.ts +2 -1
  11. package/dist/commands/mode.d.ts.map +1 -1
  12. package/dist/commands/mode.js +24 -14
  13. package/dist/commands/provenance.d.ts.map +1 -1
  14. package/dist/commands/provenance.js +216 -93
  15. package/dist/commands/quality-gates.d.ts.map +1 -1
  16. package/dist/commands/quality-gates.js +3 -1
  17. package/dist/commands/specs.d.ts.map +1 -1
  18. package/dist/commands/specs.js +184 -6
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +134 -10
  21. package/dist/commands/templates.js +2 -2
  22. package/dist/commands/worktree.d.ts +7 -0
  23. package/dist/commands/worktree.d.ts.map +1 -0
  24. package/dist/commands/worktree.js +136 -0
  25. package/dist/config/lite-scope.d.ts +33 -0
  26. package/dist/config/lite-scope.d.ts.map +1 -0
  27. package/dist/config/lite-scope.js +158 -0
  28. package/dist/config/modes.d.ts +90 -51
  29. package/dist/config/modes.d.ts.map +1 -1
  30. package/dist/config/modes.js +26 -0
  31. package/dist/error-handler.d.ts +3 -16
  32. package/dist/error-handler.d.ts.map +1 -1
  33. package/dist/error-handler.js +6 -98
  34. package/dist/generators/jest-config-generator.d.ts +32 -0
  35. package/dist/generators/jest-config-generator.d.ts.map +1 -0
  36. package/dist/generators/jest-config-generator.js +242 -0
  37. package/dist/index.js +40 -7
  38. package/dist/minimal-cli.js +3 -1
  39. package/dist/scaffold/claude-hooks.d.ts +28 -0
  40. package/dist/scaffold/claude-hooks.d.ts.map +1 -0
  41. package/dist/scaffold/claude-hooks.js +344 -0
  42. package/dist/scaffold/index.d.ts +2 -0
  43. package/dist/scaffold/index.d.ts.map +1 -1
  44. package/dist/scaffold/index.js +96 -76
  45. package/dist/templates/.caws/schemas/scope.schema.json +52 -0
  46. package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
  47. package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
  48. package/dist/templates/.claude/README.md +190 -0
  49. package/dist/templates/.claude/hooks/audit.sh +96 -0
  50. package/dist/templates/.claude/hooks/block-dangerous.sh +123 -0
  51. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  52. package/dist/templates/.claude/hooks/naming-check.sh +97 -0
  53. package/dist/templates/.claude/hooks/quality-check.sh +68 -0
  54. package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
  55. package/dist/templates/.claude/hooks/scope-guard.sh +192 -0
  56. package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
  57. package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
  58. package/dist/templates/.claude/settings.json +95 -0
  59. package/dist/templates/.cursor/README.md +0 -3
  60. package/dist/templates/.github/copilot-instructions.md +82 -0
  61. package/dist/templates/.junie/guidelines.md +73 -0
  62. package/dist/templates/.vscode/launch.json +0 -27
  63. package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  64. package/dist/templates/CLAUDE.md +101 -0
  65. package/dist/templates/agents.md +73 -1016
  66. package/dist/templates/docs/README.md +5 -5
  67. package/dist/test-analysis.d.ts +50 -1
  68. package/dist/test-analysis.d.ts.map +1 -1
  69. package/dist/test-analysis.js +203 -10
  70. package/dist/utils/error-categories.d.ts +52 -0
  71. package/dist/utils/error-categories.d.ts.map +1 -0
  72. package/dist/utils/error-categories.js +210 -0
  73. package/dist/utils/gitignore-updater.d.ts +1 -1
  74. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  75. package/dist/utils/gitignore-updater.js +4 -0
  76. package/dist/utils/ide-detection.js +133 -0
  77. package/dist/utils/quality-gates-utils.d.ts +49 -0
  78. package/dist/utils/quality-gates-utils.d.ts.map +1 -0
  79. package/dist/utils/quality-gates-utils.js +402 -0
  80. package/dist/utils/typescript-detector.d.ts +8 -5
  81. package/dist/utils/typescript-detector.d.ts.map +1 -1
  82. package/dist/utils/typescript-detector.js +36 -90
  83. package/dist/validation/spec-validation.d.ts.map +1 -1
  84. package/dist/validation/spec-validation.js +59 -6
  85. package/dist/worktree/worktree-manager.d.ts +54 -0
  86. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  87. package/dist/worktree/worktree-manager.js +378 -0
  88. package/package.json +9 -3
  89. package/templates/.caws/schemas/scope.schema.json +52 -0
  90. package/templates/.caws/schemas/working-spec.schema.json +1 -1
  91. package/templates/.caws/schemas/worktrees.schema.json +36 -0
  92. package/templates/.claude/README.md +190 -0
  93. package/templates/.claude/hooks/audit.sh +96 -0
  94. package/templates/.claude/hooks/block-dangerous.sh +123 -0
  95. package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
  96. package/templates/.claude/hooks/naming-check.sh +97 -0
  97. package/templates/.claude/hooks/quality-check.sh +68 -0
  98. package/templates/.claude/hooks/scan-secrets.sh +85 -0
  99. package/templates/.claude/hooks/scope-guard.sh +192 -0
  100. package/templates/.claude/hooks/simplification-guard.sh +92 -0
  101. package/templates/.claude/hooks/validate-spec.sh +76 -0
  102. package/templates/.claude/settings.json +95 -0
  103. package/templates/.cursor/README.md +0 -3
  104. package/templates/.github/copilot-instructions.md +82 -0
  105. package/templates/.junie/guidelines.md +73 -0
  106. package/templates/.vscode/launch.json +0 -27
  107. package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
  108. package/templates/AGENTS.md +104 -0
  109. package/templates/CLAUDE.md +101 -0
  110. package/templates/docs/README.md +5 -5
  111. package/templates/.github/copilot/instructions.md +0 -311
  112. package/templates/agents.md +0 -1047
@@ -101,10 +101,40 @@ function detectTestFramework(projectDir = process.cwd(), packageJson = null) {
101
101
  }
102
102
 
103
103
  /**
104
- * Get workspace directories from package.json
104
+ * Expand workspace glob patterns to actual directories
105
+ * Shared helper for npm, pnpm, and lerna workspace resolution
106
+ * @param {string[]} patterns - Workspace patterns (may include globs like "packages/*")
105
107
  * @param {string} projectDir - Project directory path
106
- * @returns {string[]} Array of workspace directories
108
+ * @returns {string[]} Array of resolved workspace directory paths
107
109
  */
110
+ function expandWorkspacePatterns(patterns, projectDir) {
111
+ const workspaceDirs = [];
112
+ for (const pattern of patterns) {
113
+ if (pattern.includes('*')) {
114
+ const baseDir = pattern.split('*')[0];
115
+ const fullBaseDir = path.join(projectDir, baseDir);
116
+
117
+ if (fs.existsSync(fullBaseDir)) {
118
+ const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
119
+ for (const entry of entries) {
120
+ if (entry.isDirectory()) {
121
+ const wsPath = path.join(fullBaseDir, entry.name);
122
+ if (fs.existsSync(path.join(wsPath, 'package.json'))) {
123
+ workspaceDirs.push(wsPath);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } else {
129
+ const wsPath = path.join(projectDir, pattern);
130
+ if (fs.existsSync(path.join(wsPath, 'package.json'))) {
131
+ workspaceDirs.push(wsPath);
132
+ }
133
+ }
134
+ }
135
+ return workspaceDirs;
136
+ }
137
+
108
138
  /**
109
139
  * Get workspace directories from npm/yarn package.json workspaces
110
140
  * @param {string} projectDir - Project directory path
@@ -120,36 +150,7 @@ function getNpmWorkspaces(projectDir) {
120
150
  try {
121
151
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
122
152
  const workspaces = packageJson.workspaces || [];
123
-
124
- // Convert glob patterns to actual directories (simple implementation)
125
- const workspaceDirs = [];
126
- for (const ws of workspaces) {
127
- // Handle simple patterns like "packages/*" or "iterations/*"
128
- if (ws.includes('*')) {
129
- const baseDir = ws.split('*')[0];
130
- const fullBaseDir = path.join(projectDir, baseDir);
131
-
132
- if (fs.existsSync(fullBaseDir)) {
133
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
134
- for (const entry of entries) {
135
- if (entry.isDirectory()) {
136
- const wsPath = path.join(fullBaseDir, entry.name);
137
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
138
- workspaceDirs.push(wsPath);
139
- }
140
- }
141
- }
142
- }
143
- } else {
144
- // Direct path
145
- const wsPath = path.join(projectDir, ws);
146
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
147
- workspaceDirs.push(wsPath);
148
- }
149
- }
150
- }
151
-
152
- return workspaceDirs;
153
+ return expandWorkspacePatterns(workspaces, projectDir);
153
154
  } catch (error) {
154
155
  return [];
155
156
  }
@@ -171,35 +172,7 @@ function getPnpmWorkspaces(projectDir) {
171
172
  const yaml = require('js-yaml');
172
173
  const config = yaml.load(fs.readFileSync(pnpmFile, 'utf8'));
173
174
  const workspacePatterns = config.packages || [];
174
-
175
- // Convert glob patterns to actual directories
176
- const workspaceDirs = [];
177
- for (const pattern of workspacePatterns) {
178
- if (pattern.includes('*')) {
179
- const baseDir = pattern.split('*')[0];
180
- const fullBaseDir = path.join(projectDir, baseDir);
181
-
182
- if (fs.existsSync(fullBaseDir)) {
183
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
184
- for (const entry of entries) {
185
- if (entry.isDirectory()) {
186
- const wsPath = path.join(fullBaseDir, entry.name);
187
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
188
- workspaceDirs.push(wsPath);
189
- }
190
- }
191
- }
192
- }
193
- } else {
194
- // Direct path
195
- const wsPath = path.join(projectDir, pattern);
196
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
197
- workspaceDirs.push(wsPath);
198
- }
199
- }
200
- }
201
-
202
- return workspaceDirs;
175
+ return expandWorkspacePatterns(workspacePatterns, projectDir);
203
176
  } catch (error) {
204
177
  return [];
205
178
  }
@@ -220,35 +193,7 @@ function getLernaWorkspaces(projectDir) {
220
193
  try {
221
194
  const config = JSON.parse(fs.readFileSync(lernaFile, 'utf8'));
222
195
  const workspacePatterns = config.packages || ['packages/*'];
223
-
224
- // Convert glob patterns to actual directories
225
- const workspaceDirs = [];
226
- for (const pattern of workspacePatterns) {
227
- if (pattern.includes('*')) {
228
- const baseDir = pattern.split('*')[0];
229
- const fullBaseDir = path.join(projectDir, baseDir);
230
-
231
- if (fs.existsSync(fullBaseDir)) {
232
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
233
- for (const entry of entries) {
234
- if (entry.isDirectory()) {
235
- const wsPath = path.join(fullBaseDir, entry.name);
236
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
237
- workspaceDirs.push(wsPath);
238
- }
239
- }
240
- }
241
- }
242
- } else {
243
- // Direct path
244
- const wsPath = path.join(projectDir, pattern);
245
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
246
- workspaceDirs.push(wsPath);
247
- }
248
- }
249
- }
250
-
251
- return workspaceDirs;
196
+ return expandWorkspacePatterns(workspacePatterns, projectDir);
252
197
  } catch (error) {
253
198
  return [];
254
199
  }
@@ -416,6 +361,7 @@ module.exports = {
416
361
  getNpmWorkspaces,
417
362
  getPnpmWorkspaces,
418
363
  getLernaWorkspaces,
364
+ expandWorkspacePatterns,
419
365
  checkHoistedDependency,
420
366
  checkTypeScriptTestConfig,
421
367
  generateRecommendations,
@@ -1 +1 @@
1
- {"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,mEA8HC;AAED;;;;;GAKG;AACH,kFAgdC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
1
+ {"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,mEA8HC;AAED;;;;;GAKG;AACH,kFAgdC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
@@ -5,6 +5,59 @@
5
5
  */
6
6
 
7
7
  const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
8
+ const { execSync } = require('child_process');
9
+
10
+ /**
11
+ * Get actual budget statistics from git history
12
+ * Analyzes changes since last tag or initial commit
13
+ * @param {string} specDir - Project directory
14
+ * @returns {Object|null} Budget stats or null on failure
15
+ */
16
+ function getActualBudgetStats(specDir) {
17
+ const cwd = specDir || process.cwd();
18
+ try {
19
+ // Get base ref (last tag or initial commit)
20
+ let baseRef;
21
+ try {
22
+ baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
23
+ cwd,
24
+ encoding: 'utf8'
25
+ }).trim();
26
+ } catch {
27
+ // No tags found, use initial commit
28
+ baseRef = execSync('git rev-list --max-parents=0 HEAD', {
29
+ cwd,
30
+ encoding: 'utf8'
31
+ }).trim();
32
+ }
33
+
34
+ // Count files changed since base ref
35
+ const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
36
+ cwd,
37
+ encoding: 'utf8'
38
+ });
39
+ const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
40
+
41
+ // Count lines changed (added + removed)
42
+ const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
43
+ cwd,
44
+ encoding: 'utf8'
45
+ });
46
+ let lines_changed = 0;
47
+ for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
48
+ const [added, removed] = line.split('\t');
49
+ // Handle binary files (shown as '-')
50
+ const addedNum = added === '-' ? 0 : parseInt(added, 10) || 0;
51
+ const removedNum = removed === '-' ? 0 : parseInt(removed, 10) || 0;
52
+ lines_changed += addedNum + removedNum;
53
+ }
54
+
55
+ return { files_changed, lines_changed };
56
+ } catch {
57
+ // Git not available or not a repository
58
+ return null;
59
+ }
60
+ }
8
61
 
9
62
  /**
10
63
  * Basic validation of working spec
@@ -511,14 +564,14 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
511
564
  try {
512
565
  const derivedBudget = deriveBudget(spec, projectRoot);
513
566
 
514
- // Mock current stats for now - in real implementation this would analyze git changes
515
- const mockStats = {
516
- files_changed: 50, // This would be calculated from actual changes
517
- lines_changed: 5000,
518
- risk_tier: spec.risk_tier,
567
+ // Get actual stats from git history
568
+ const actualStats = getActualBudgetStats(projectRoot) || {
569
+ files_changed: 0,
570
+ lines_changed: 0,
519
571
  };
572
+ actualStats.risk_tier = spec.risk_tier;
520
573
 
521
- budgetCheck = checkBudgetCompliance(derivedBudget, mockStats);
574
+ budgetCheck = checkBudgetCompliance(derivedBudget, actualStats);
522
575
 
523
576
  if (!budgetCheck.compliant) {
524
577
  for (const violation of budgetCheck.violations) {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Create a new git worktree with scope isolation
3
+ * @param {string} name - Worktree name
4
+ * @param {Object} options - Creation options
5
+ * @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
6
+ * @param {string} [options.baseBranch] - Base branch to create from
7
+ * @param {string} [options.specId] - Associated spec ID for standard+ modes
8
+ * @returns {Object} Created worktree info
9
+ */
10
+ export function createWorktree(name: string, options?: {
11
+ scope?: string;
12
+ baseBranch?: string;
13
+ specId?: string;
14
+ }): any;
15
+ /**
16
+ * List all registered worktrees with filesystem validation
17
+ * @returns {Array} Worktree entries with status
18
+ */
19
+ export function listWorktrees(): any[];
20
+ /**
21
+ * Destroy a worktree
22
+ * @param {string} name - Worktree name
23
+ * @param {Object} options - Destruction options
24
+ * @param {boolean} [options.deleteBranch] - Also delete the branch
25
+ * @param {boolean} [options.force] - Force removal even if dirty
26
+ */
27
+ export function destroyWorktree(name: string, options?: {
28
+ deleteBranch?: boolean;
29
+ force?: boolean;
30
+ }): void;
31
+ /**
32
+ * Prune stale worktree entries
33
+ * @param {Object} options - Prune options
34
+ * @param {number} [options.maxAgeDays] - Remove entries older than this many days
35
+ * @returns {Array} Pruned entries
36
+ */
37
+ export function pruneWorktrees(options?: {
38
+ maxAgeDays?: number;
39
+ }): any[];
40
+ /**
41
+ * Load the worktree registry
42
+ * @param {string} root - Repository root
43
+ * @returns {Object} Registry object
44
+ */
45
+ export function loadRegistry(root: string): any;
46
+ /**
47
+ * Get the git repository root
48
+ * @returns {string} Absolute path to repo root
49
+ */
50
+ export function getRepoRoot(): string;
51
+ export const WORKTREES_DIR: ".caws/worktrees";
52
+ export const REGISTRY_FILE: ".caws/worktrees.json";
53
+ export const BRANCH_PREFIX: "caws/";
54
+ //# sourceMappingURL=worktree-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,OA4IF;AAED;;;GAGG;AACH,uCAqCC;AAED;;;;;;GAMG;AACH,sCALW,MAAM,YAEd;IAA0B,YAAY,GAA9B,OAAO;IACW,KAAK,GAAvB,OAAO;CACjB,QAqDA;AAED;;;;;GAKG;AACH,yCAHG;IAAyB,UAAU,GAA3B,MAAM;CACd,SA6CF;AA1UD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AAnCD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,4BAAsB,iBAAiB,CAAC;AACxC,4BAAsB,sBAAsB,CAAC;AAC7C,4BAAsB,OAAO,CAAC"}
@@ -0,0 +1,378 @@
1
+ /**
2
+ * @fileoverview CAWS Git Worktree Manager
3
+ * Provides CRUD operations for git worktrees with scope isolation
4
+ * @author @darianrosebrook
5
+ */
6
+
7
+ const { execFileSync } = require('child_process');
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+
12
+ const WORKTREES_DIR = '.caws/worktrees';
13
+ const REGISTRY_FILE = '.caws/worktrees.json';
14
+ const BRANCH_PREFIX = 'caws/';
15
+
16
+ /**
17
+ * Get the git repository root
18
+ * @returns {string} Absolute path to repo root
19
+ */
20
+ function getRepoRoot() {
21
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
22
+ encoding: 'utf8',
23
+ }).trim();
24
+ }
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 worktree registry
38
+ * @param {string} root - Repository root
39
+ * @returns {Object} Registry object
40
+ */
41
+ function loadRegistry(root) {
42
+ const registryPath = path.join(root, REGISTRY_FILE);
43
+ try {
44
+ if (fs.existsSync(registryPath)) {
45
+ return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
46
+ }
47
+ } catch {
48
+ // Corrupted registry, start fresh
49
+ }
50
+ return { version: 1, worktrees: {} };
51
+ }
52
+
53
+ /**
54
+ * Save the worktree registry
55
+ * @param {string} root - Repository root
56
+ * @param {Object} registry - Registry object
57
+ */
58
+ function saveRegistry(root, registry) {
59
+ const registryPath = path.join(root, REGISTRY_FILE);
60
+ fs.ensureDirSync(path.dirname(registryPath));
61
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
62
+ }
63
+
64
+ /**
65
+ * Create a new git worktree with scope isolation
66
+ * @param {string} name - Worktree name
67
+ * @param {Object} options - Creation options
68
+ * @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
69
+ * @param {string} [options.baseBranch] - Base branch to create from
70
+ * @param {string} [options.specId] - Associated spec ID for standard+ modes
71
+ * @returns {Object} Created worktree info
72
+ */
73
+ function createWorktree(name, options = {}) {
74
+ const root = getRepoRoot();
75
+ const { scope, baseBranch, specId } = options;
76
+
77
+ // Validate name
78
+ if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
79
+ throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
80
+ }
81
+
82
+ const registry = loadRegistry(root);
83
+
84
+ // Check for duplicate
85
+ if (registry.worktrees[name]) {
86
+ throw new Error(`Worktree '${name}' already exists. Use 'caws worktree destroy ${name}' first.`);
87
+ }
88
+
89
+ const worktreePath = path.join(root, WORKTREES_DIR, name);
90
+ const branchName = BRANCH_PREFIX + name;
91
+ const base = baseBranch || getCurrentBranch();
92
+
93
+ // Create the worktree directory
94
+ fs.ensureDirSync(path.dirname(worktreePath));
95
+
96
+ // Create git worktree with new branch
97
+ try {
98
+ execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
99
+ cwd: root,
100
+ stdio: 'pipe',
101
+ });
102
+ } catch (error) {
103
+ // Branch might already exist
104
+ if (error.message.includes('already exists')) {
105
+ execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
106
+ cwd: root,
107
+ stdio: 'pipe',
108
+ });
109
+ } else {
110
+ throw new Error(`Failed to create worktree: ${error.message}`);
111
+ }
112
+ }
113
+
114
+ // Set up sparse checkout if scope is provided
115
+ if (scope) {
116
+ try {
117
+ execFileSync('git', ['sparse-checkout', 'init', '--cone'], {
118
+ cwd: worktreePath,
119
+ stdio: 'pipe',
120
+ });
121
+
122
+ // Parse scope patterns (comma-separated)
123
+ const patterns = scope.split(',').map((p) => p.trim());
124
+ execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
125
+ cwd: worktreePath,
126
+ stdio: 'pipe',
127
+ });
128
+ } catch (error) {
129
+ console.warn(chalk.yellow(`⚠️ Sparse checkout setup failed: ${error.message}`));
130
+ console.warn(chalk.blue('💡 Worktree created but without sparse checkout'));
131
+ }
132
+ }
133
+
134
+ // Copy .caws/ config into worktree
135
+ const cawsSource = path.join(root, '.caws');
136
+ const cawsDest = path.join(worktreePath, '.caws');
137
+ if (fs.existsSync(cawsSource)) {
138
+ try {
139
+ fs.copySync(cawsSource, cawsDest, {
140
+ filter: (src) => {
141
+ // Don't copy worktrees directory or registry into the worktree
142
+ const rel = path.relative(cawsSource, src);
143
+ return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
144
+ },
145
+ });
146
+ } catch {
147
+ // Non-fatal
148
+ }
149
+ }
150
+
151
+ // Generate working spec if in standard+ mode and specId provided
152
+ if (specId) {
153
+ try {
154
+ const { generateWorkingSpec } = require('../generators/working-spec');
155
+ const specContent = generateWorkingSpec({
156
+ projectId: specId,
157
+ projectTitle: `Worktree: ${name}`,
158
+ projectDescription: `Isolated worktree for ${name}`,
159
+ riskTier: 3,
160
+ projectMode: 'feature',
161
+ scopeIn: scope || 'src/',
162
+ scopeOut: 'node_modules/, dist/, build/',
163
+ maxFiles: 25,
164
+ maxLoc: 1000,
165
+ blastModules: scope || 'src',
166
+ dataMigration: false,
167
+ rollbackSlo: '5m',
168
+ projectThreats: '',
169
+ projectInvariants: 'System maintains data consistency',
170
+ acceptanceCriteria: 'Given current state, when action occurs, then expected result',
171
+ a11yRequirements: 'keyboard',
172
+ perfBudget: 250,
173
+ securityRequirements: 'validation',
174
+ contractType: '',
175
+ contractPath: '',
176
+ observabilityLogs: '',
177
+ observabilityMetrics: '',
178
+ observabilityTraces: '',
179
+ migrationPlan: '',
180
+ rollbackPlan: '',
181
+ needsOverride: false,
182
+ isExperimental: false,
183
+ aiConfidence: 0.8,
184
+ uncertaintyAreas: '',
185
+ complexityFactors: '',
186
+ });
187
+ const specPath = path.join(cawsDest, 'working-spec.yaml');
188
+ fs.ensureDirSync(path.dirname(specPath));
189
+ fs.writeFileSync(specPath, specContent);
190
+ } catch {
191
+ // Non-fatal: spec generation is optional
192
+ }
193
+ }
194
+
195
+ // Register worktree
196
+ const entry = {
197
+ name,
198
+ path: worktreePath,
199
+ branch: branchName,
200
+ baseBranch: base,
201
+ scope: scope || null,
202
+ specId: specId || null,
203
+ createdAt: new Date().toISOString(),
204
+ status: 'active',
205
+ };
206
+
207
+ registry.worktrees[name] = entry;
208
+ saveRegistry(root, registry);
209
+
210
+ return entry;
211
+ }
212
+
213
+ /**
214
+ * List all registered worktrees with filesystem validation
215
+ * @returns {Array} Worktree entries with status
216
+ */
217
+ function listWorktrees() {
218
+ const root = getRepoRoot();
219
+ const registry = loadRegistry(root);
220
+
221
+ // Get actual git worktrees for validation
222
+ let gitWorktrees = [];
223
+ try {
224
+ const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
225
+ cwd: root,
226
+ encoding: 'utf8',
227
+ });
228
+ gitWorktrees = output
229
+ .split('\n\n')
230
+ .filter(Boolean)
231
+ .map((block) => {
232
+ const lines = block.split('\n');
233
+ const worktreeLine = lines.find((l) => l.startsWith('worktree '));
234
+ return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
235
+ })
236
+ .filter(Boolean);
237
+ } catch {
238
+ // Git worktree list failed
239
+ }
240
+
241
+ const entries = Object.values(registry.worktrees).map((entry) => {
242
+ const exists = fs.existsSync(entry.path);
243
+ const inGit = gitWorktrees.some(
244
+ (wt) => path.resolve(wt) === path.resolve(entry.path)
245
+ );
246
+
247
+ return {
248
+ ...entry,
249
+ status: exists && inGit ? 'active' : exists ? 'orphaned' : 'missing',
250
+ };
251
+ });
252
+
253
+ return entries;
254
+ }
255
+
256
+ /**
257
+ * Destroy a worktree
258
+ * @param {string} name - Worktree name
259
+ * @param {Object} options - Destruction options
260
+ * @param {boolean} [options.deleteBranch] - Also delete the branch
261
+ * @param {boolean} [options.force] - Force removal even if dirty
262
+ */
263
+ function destroyWorktree(name, options = {}) {
264
+ const root = getRepoRoot();
265
+ const registry = loadRegistry(root);
266
+ const { deleteBranch = false, force = false } = options;
267
+
268
+ const entry = registry.worktrees[name];
269
+ if (!entry) {
270
+ throw new Error(`Worktree '${name}' not found in registry`);
271
+ }
272
+
273
+ // Remove git worktree
274
+ try {
275
+ const args = ['worktree', 'remove'];
276
+ if (force) args.push('--force');
277
+ args.push(entry.path);
278
+ execFileSync('git', args, { cwd: root, stdio: 'pipe' });
279
+ } catch (error) {
280
+ if (force) {
281
+ // Force cleanup: remove directory manually
282
+ if (fs.existsSync(entry.path)) {
283
+ fs.removeSync(entry.path);
284
+ }
285
+ // Prune git worktree list
286
+ try {
287
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
288
+ } catch {
289
+ // Non-fatal
290
+ }
291
+ } else {
292
+ throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
293
+ }
294
+ }
295
+
296
+ // Optionally delete branch
297
+ if (deleteBranch && entry.branch) {
298
+ try {
299
+ execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
300
+ } catch {
301
+ if (force) {
302
+ try {
303
+ execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
304
+ } catch {
305
+ // Non-fatal
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ // Update registry
312
+ registry.worktrees[name].status = 'destroyed';
313
+ registry.worktrees[name].destroyedAt = new Date().toISOString();
314
+ saveRegistry(root, registry);
315
+ }
316
+
317
+ /**
318
+ * Prune stale worktree entries
319
+ * @param {Object} options - Prune options
320
+ * @param {number} [options.maxAgeDays] - Remove entries older than this many days
321
+ * @returns {Array} Pruned entries
322
+ */
323
+ function pruneWorktrees(options = {}) {
324
+ const root = getRepoRoot();
325
+ const registry = loadRegistry(root);
326
+ const { maxAgeDays = 30 } = options;
327
+
328
+ const now = new Date();
329
+ const pruned = [];
330
+
331
+ for (const [name, entry] of Object.entries(registry.worktrees)) {
332
+ const created = new Date(entry.createdAt);
333
+ const ageDays = (now - created) / (1000 * 60 * 60 * 24);
334
+
335
+ const shouldPrune =
336
+ entry.status === 'destroyed' ||
337
+ (!fs.existsSync(entry.path) && ageDays > maxAgeDays) ||
338
+ (maxAgeDays === 0 && entry.status === 'destroyed');
339
+
340
+ if (shouldPrune) {
341
+ // Clean up filesystem if still exists
342
+ if (fs.existsSync(entry.path)) {
343
+ try {
344
+ execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
345
+ cwd: root,
346
+ stdio: 'pipe',
347
+ });
348
+ } catch {
349
+ fs.removeSync(entry.path);
350
+ }
351
+ }
352
+ pruned.push(entry);
353
+ delete registry.worktrees[name];
354
+ }
355
+ }
356
+
357
+ // Prune git's worktree list
358
+ try {
359
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
360
+ } catch {
361
+ // Non-fatal
362
+ }
363
+
364
+ saveRegistry(root, registry);
365
+ return pruned;
366
+ }
367
+
368
+ module.exports = {
369
+ createWorktree,
370
+ listWorktrees,
371
+ destroyWorktree,
372
+ pruneWorktrees,
373
+ loadRegistry,
374
+ getRepoRoot,
375
+ WORKTREES_DIR,
376
+ REGISTRY_FILE,
377
+ BRANCH_PREFIX,
378
+ };