@paths.design/caws-cli 9.3.2 → 10.0.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.
Files changed (273) hide show
  1. package/README.md +58 -27
  2. package/dist/commands/archive.js +67 -28
  3. package/dist/commands/burnup.js +20 -11
  4. package/dist/commands/diagnose.js +34 -22
  5. package/dist/commands/evaluate.js +27 -15
  6. package/dist/commands/gates.js +122 -0
  7. package/dist/commands/init.js +143 -15
  8. package/dist/commands/iterate.js +77 -4
  9. package/dist/commands/parallel.js +4 -0
  10. package/dist/commands/plan.js +9 -19
  11. package/dist/commands/provenance.js +53 -17
  12. package/dist/commands/quality-monitor.js +64 -45
  13. package/dist/commands/sidecar.js +71 -0
  14. package/dist/commands/specs.js +233 -44
  15. package/dist/commands/status.js +113 -9
  16. package/dist/commands/tutorial.js +10 -9
  17. package/dist/commands/validate.js +49 -6
  18. package/dist/commands/verify-acs.js +35 -78
  19. package/dist/commands/waivers.js +69 -12
  20. package/dist/commands/worktree.js +50 -25
  21. package/dist/error-handler.js +2 -13
  22. package/dist/gates/budget-limit.js +116 -0
  23. package/dist/gates/feedback.js +260 -0
  24. package/dist/gates/format.js +179 -0
  25. package/dist/gates/god-object.js +117 -0
  26. package/dist/gates/pipeline.js +167 -0
  27. package/dist/gates/scope-boundary.js +93 -0
  28. package/dist/gates/spec-completeness.js +102 -0
  29. package/dist/gates/todo-detection.js +205 -0
  30. package/dist/index.js +130 -151
  31. package/dist/parallel/parallel-manager.js +3 -3
  32. package/dist/policy/PolicyManager.js +42 -10
  33. package/dist/scaffold/claude-hooks.js +24 -1
  34. package/dist/scaffold/git-hooks.js +45 -102
  35. package/dist/scaffold/index.js +4 -3
  36. package/dist/session/session-manager.js +71 -14
  37. package/dist/sidecars/index.js +33 -0
  38. package/dist/sidecars/listeners.js +40 -0
  39. package/dist/sidecars/provenance-summary.js +238 -0
  40. package/dist/sidecars/quality-gaps.js +258 -0
  41. package/dist/sidecars/schema.js +149 -0
  42. package/dist/sidecars/spec-drift.js +151 -0
  43. package/dist/sidecars/waiver-draft.js +176 -0
  44. package/dist/templates/.caws/schemas/policy.schema.json +50 -0
  45. package/dist/templates/.caws/schemas/waivers.schema.json +30 -24
  46. package/dist/templates/.caws/schemas/working-spec.schema.json +51 -8
  47. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  48. package/dist/templates/.caws/templates/working-spec.template.yml +7 -3
  49. package/dist/templates/.claude/hooks/audit.sh +0 -0
  50. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  51. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  52. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  53. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  54. package/dist/templates/.claude/hooks/scope-guard.sh +34 -32
  55. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  56. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  57. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  58. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  59. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  60. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  61. package/dist/templates/.claude/hooks/worktree-write-guard.sh +1 -1
  62. package/dist/templates/.claude/settings.json +26 -0
  63. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  64. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  65. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  66. package/dist/templates/.cursor/hooks.json +25 -0
  67. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  68. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  69. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  70. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  71. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  72. package/dist/templates/.github/copilot-instructions.md +5 -5
  73. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  74. package/dist/templates/.junie/guidelines.md +2 -2
  75. package/dist/templates/.vscode/settings.json +3 -1
  76. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  77. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  78. package/dist/templates/CLAUDE.md +43 -8
  79. package/dist/templates/agents.md +29 -9
  80. package/dist/templates/docs/README.md +8 -7
  81. package/dist/templates/scripts/new_feature.sh +80 -0
  82. package/dist/test-analysis.js +43 -30
  83. package/dist/tool-loader.js +1 -1
  84. package/dist/utils/agent-session.js +202 -0
  85. package/dist/utils/detection.js +8 -2
  86. package/dist/utils/finalization.js +7 -6
  87. package/dist/utils/gitignore-updater.js +3 -0
  88. package/dist/utils/lifecycle-events.js +94 -0
  89. package/dist/utils/quality-gates-utils.js +29 -44
  90. package/dist/utils/schema-validator.js +42 -0
  91. package/dist/utils/spec-resolver.js +93 -21
  92. package/dist/utils/working-state.js +505 -0
  93. package/dist/validation/spec-validation.js +92 -22
  94. package/dist/waivers-manager.js +60 -6
  95. package/dist/worktree/worktree-manager.js +390 -93
  96. package/package.json +6 -6
  97. package/templates/.caws/schemas/policy.schema.json +50 -0
  98. package/templates/.caws/schemas/waivers.schema.json +30 -24
  99. package/templates/.caws/schemas/working-spec.schema.json +51 -8
  100. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  101. package/templates/.caws/templates/working-spec.template.yml +7 -3
  102. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  103. package/templates/.claude/hooks/classify_command.py +592 -0
  104. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  105. package/templates/.claude/hooks/quality-check.sh +23 -10
  106. package/templates/.claude/hooks/scope-guard.sh +34 -32
  107. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  108. package/templates/.claude/hooks/session-log.sh +76 -3
  109. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  110. package/templates/.claude/hooks/test_classify_command.py +370 -0
  111. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  112. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  113. package/templates/.claude/hooks/worktree-write-guard.sh +1 -1
  114. package/templates/.claude/settings.json +26 -0
  115. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  116. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  117. package/templates/.cursor/hooks/session-log.sh +924 -0
  118. package/templates/.cursor/hooks.json +25 -0
  119. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  120. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  121. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  122. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  123. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  124. package/templates/.github/copilot-instructions.md +5 -5
  125. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  126. package/templates/.junie/guidelines.md +2 -2
  127. package/templates/.vscode/settings.json +3 -1
  128. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  129. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  130. package/templates/CLAUDE.md +43 -8
  131. package/templates/{AGENTS.md → agents.md} +29 -9
  132. package/templates/docs/README.md +8 -7
  133. package/templates/scripts/new_feature.sh +80 -0
  134. package/dist/budget-derivation.d.ts +0 -74
  135. package/dist/budget-derivation.d.ts.map +0 -1
  136. package/dist/cicd-optimizer.d.ts +0 -142
  137. package/dist/cicd-optimizer.d.ts.map +0 -1
  138. package/dist/commands/archive.d.ts +0 -51
  139. package/dist/commands/archive.d.ts.map +0 -1
  140. package/dist/commands/burnup.d.ts +0 -6
  141. package/dist/commands/burnup.d.ts.map +0 -1
  142. package/dist/commands/diagnose.d.ts +0 -52
  143. package/dist/commands/diagnose.d.ts.map +0 -1
  144. package/dist/commands/evaluate.d.ts +0 -8
  145. package/dist/commands/evaluate.d.ts.map +0 -1
  146. package/dist/commands/init.d.ts +0 -5
  147. package/dist/commands/init.d.ts.map +0 -1
  148. package/dist/commands/iterate.d.ts +0 -8
  149. package/dist/commands/iterate.d.ts.map +0 -1
  150. package/dist/commands/mode.d.ts +0 -25
  151. package/dist/commands/mode.d.ts.map +0 -1
  152. package/dist/commands/parallel.d.ts +0 -7
  153. package/dist/commands/parallel.d.ts.map +0 -1
  154. package/dist/commands/plan.d.ts +0 -49
  155. package/dist/commands/plan.d.ts.map +0 -1
  156. package/dist/commands/provenance.d.ts +0 -32
  157. package/dist/commands/provenance.d.ts.map +0 -1
  158. package/dist/commands/quality-gates.d.ts +0 -6
  159. package/dist/commands/quality-gates.d.ts.map +0 -1
  160. package/dist/commands/quality-gates.js +0 -444
  161. package/dist/commands/quality-monitor.d.ts +0 -17
  162. package/dist/commands/quality-monitor.d.ts.map +0 -1
  163. package/dist/commands/session.d.ts +0 -7
  164. package/dist/commands/session.d.ts.map +0 -1
  165. package/dist/commands/specs.d.ts +0 -77
  166. package/dist/commands/specs.d.ts.map +0 -1
  167. package/dist/commands/status.d.ts +0 -44
  168. package/dist/commands/status.d.ts.map +0 -1
  169. package/dist/commands/templates.d.ts +0 -74
  170. package/dist/commands/templates.d.ts.map +0 -1
  171. package/dist/commands/tool.d.ts +0 -13
  172. package/dist/commands/tool.d.ts.map +0 -1
  173. package/dist/commands/troubleshoot.d.ts +0 -8
  174. package/dist/commands/troubleshoot.d.ts.map +0 -1
  175. package/dist/commands/troubleshoot.js +0 -104
  176. package/dist/commands/tutorial.d.ts +0 -55
  177. package/dist/commands/tutorial.d.ts.map +0 -1
  178. package/dist/commands/validate.d.ts +0 -15
  179. package/dist/commands/validate.d.ts.map +0 -1
  180. package/dist/commands/waivers.d.ts +0 -8
  181. package/dist/commands/waivers.d.ts.map +0 -1
  182. package/dist/commands/workflow.d.ts +0 -85
  183. package/dist/commands/workflow.d.ts.map +0 -1
  184. package/dist/commands/worktree.d.ts +0 -7
  185. package/dist/commands/worktree.d.ts.map +0 -1
  186. package/dist/config/index.d.ts +0 -29
  187. package/dist/config/index.d.ts.map +0 -1
  188. package/dist/config/lite-scope.d.ts +0 -33
  189. package/dist/config/lite-scope.d.ts.map +0 -1
  190. package/dist/config/modes.d.ts +0 -264
  191. package/dist/config/modes.d.ts.map +0 -1
  192. package/dist/constants/spec-types.d.ts +0 -93
  193. package/dist/constants/spec-types.d.ts.map +0 -1
  194. package/dist/error-handler.d.ts +0 -151
  195. package/dist/error-handler.d.ts.map +0 -1
  196. package/dist/generators/jest-config-generator.d.ts +0 -32
  197. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  198. package/dist/generators/jest-config.d.ts +0 -32
  199. package/dist/generators/jest-config.d.ts.map +0 -1
  200. package/dist/generators/jest-config.js +0 -242
  201. package/dist/generators/working-spec.d.ts +0 -13
  202. package/dist/generators/working-spec.d.ts.map +0 -1
  203. package/dist/index-new.d.ts +0 -5
  204. package/dist/index-new.d.ts.map +0 -1
  205. package/dist/index-new.js +0 -317
  206. package/dist/index.d.ts +0 -5
  207. package/dist/index.d.ts.map +0 -1
  208. package/dist/index.js.backup +0 -4711
  209. package/dist/minimal-cli.d.ts +0 -3
  210. package/dist/minimal-cli.d.ts.map +0 -1
  211. package/dist/parallel/parallel-manager.d.ts +0 -67
  212. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  213. package/dist/policy/PolicyManager.d.ts +0 -104
  214. package/dist/policy/PolicyManager.d.ts.map +0 -1
  215. package/dist/scaffold/claude-hooks.d.ts +0 -28
  216. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  217. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  218. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  219. package/dist/scaffold/git-hooks.d.ts +0 -38
  220. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  221. package/dist/scaffold/index.d.ts +0 -17
  222. package/dist/scaffold/index.d.ts.map +0 -1
  223. package/dist/session/session-manager.d.ts +0 -94
  224. package/dist/session/session-manager.d.ts.map +0 -1
  225. package/dist/spec/SpecFileManager.d.ts +0 -146
  226. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  227. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  228. package/dist/templates/.github/copilot/instructions.md +0 -311
  229. package/dist/test-analysis.d.ts +0 -231
  230. package/dist/test-analysis.d.ts.map +0 -1
  231. package/dist/tool-interface.d.ts +0 -236
  232. package/dist/tool-interface.d.ts.map +0 -1
  233. package/dist/tool-loader.d.ts +0 -77
  234. package/dist/tool-loader.d.ts.map +0 -1
  235. package/dist/tool-validator.d.ts +0 -72
  236. package/dist/tool-validator.d.ts.map +0 -1
  237. package/dist/utils/async-utils.d.ts +0 -73
  238. package/dist/utils/async-utils.d.ts.map +0 -1
  239. package/dist/utils/command-wrapper.d.ts +0 -66
  240. package/dist/utils/command-wrapper.d.ts.map +0 -1
  241. package/dist/utils/detection.d.ts +0 -14
  242. package/dist/utils/detection.d.ts.map +0 -1
  243. package/dist/utils/error-categories.d.ts +0 -52
  244. package/dist/utils/error-categories.d.ts.map +0 -1
  245. package/dist/utils/finalization.d.ts +0 -17
  246. package/dist/utils/finalization.d.ts.map +0 -1
  247. package/dist/utils/git-lock.d.ts +0 -13
  248. package/dist/utils/git-lock.d.ts.map +0 -1
  249. package/dist/utils/gitignore-updater.d.ts +0 -39
  250. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  251. package/dist/utils/ide-detection.d.ts +0 -89
  252. package/dist/utils/ide-detection.d.ts.map +0 -1
  253. package/dist/utils/project-analysis.d.ts +0 -34
  254. package/dist/utils/project-analysis.d.ts.map +0 -1
  255. package/dist/utils/promise-utils.d.ts +0 -30
  256. package/dist/utils/promise-utils.d.ts.map +0 -1
  257. package/dist/utils/quality-gates-utils.d.ts +0 -49
  258. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  259. package/dist/utils/quality-gates.d.ts +0 -49
  260. package/dist/utils/quality-gates.d.ts.map +0 -1
  261. package/dist/utils/quality-gates.js +0 -402
  262. package/dist/utils/spec-resolver.d.ts +0 -80
  263. package/dist/utils/spec-resolver.d.ts.map +0 -1
  264. package/dist/utils/typescript-detector.d.ts +0 -66
  265. package/dist/utils/typescript-detector.d.ts.map +0 -1
  266. package/dist/utils/yaml-validation.d.ts +0 -32
  267. package/dist/utils/yaml-validation.d.ts.map +0 -1
  268. package/dist/validation/spec-validation.d.ts +0 -43
  269. package/dist/validation/spec-validation.d.ts.map +0 -1
  270. package/dist/waivers-manager.d.ts +0 -167
  271. package/dist/waivers-manager.d.ts.map +0 -1
  272. package/dist/worktree/worktree-manager.d.ts +0 -54
  273. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -8,11 +8,88 @@ const { execFileSync } = require('child_process');
8
8
  const fs = require('fs-extra');
9
9
  const path = require('path');
10
10
  const chalk = require('chalk');
11
+ const { createValidator, getSchemaPath } = require('../utils/schema-validator');
12
+ const { getAgentSessionId } = require('../utils/agent-session');
13
+ const { lifecycle, EVENTS } = require('../utils/lifecycle-events');
11
14
 
12
15
  const WORKTREES_DIR = '.caws/worktrees';
13
16
  const REGISTRY_FILE = '.caws/worktrees.json';
14
17
  const BRANCH_PREFIX = 'caws/';
15
18
 
19
+ function findFeatureSpecPath(root, specId) {
20
+ if (!specId) return null;
21
+
22
+ const candidates = [
23
+ path.join(root, '.caws', 'specs', `${specId}.yaml`),
24
+ path.join(root, '.caws', 'specs', `${specId}.yml`),
25
+ ];
26
+
27
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
28
+ }
29
+
30
+ function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
31
+ if (!specId) return;
32
+
33
+ const canonicalSpecPath = findFeatureSpecPath(root, specId);
34
+ const workingSpecPath = path.join(cawsDest, 'working-spec.yaml');
35
+
36
+ if (!canonicalSpecPath) {
37
+ console.warn(
38
+ chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default working spec for worktree`)
39
+ );
40
+ }
41
+
42
+ if (canonicalSpecPath) {
43
+ const destSpecsDir = path.join(cawsDest, 'specs');
44
+ const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
45
+ fs.ensureDirSync(destSpecsDir);
46
+
47
+ // Keep a canonical feature-spec copy inside the worktree and align
48
+ // working-spec.yaml to that exact content for legacy-compatible commands.
49
+ const specContent = fs.readFileSync(canonicalSpecPath, 'utf8');
50
+ fs.writeFileSync(destSpecPath, specContent);
51
+ fs.writeFileSync(workingSpecPath, specContent);
52
+ return;
53
+ }
54
+
55
+ const { generateWorkingSpec } = require('../generators/working-spec');
56
+ const specContent = generateWorkingSpec({
57
+ projectId: specId,
58
+ projectTitle: `Worktree: ${worktreeName}`,
59
+ projectDescription: `Isolated worktree for ${worktreeName}`,
60
+ riskTier: 3,
61
+ projectMode: 'feature',
62
+ scopeIn: scope || 'src/',
63
+ scopeOut: 'node_modules/, dist/, build/',
64
+ maxFiles: 25,
65
+ maxLoc: 1000,
66
+ blastModules: scope || 'src',
67
+ dataMigration: false,
68
+ rollbackSlo: '5m',
69
+ projectThreats: '',
70
+ projectInvariants: 'System maintains data consistency',
71
+ acceptanceCriteria: 'Given current state, when action occurs, then expected result',
72
+ a11yRequirements: 'keyboard',
73
+ perfBudget: 250,
74
+ securityRequirements: 'validation',
75
+ contractType: '',
76
+ contractPath: '',
77
+ observabilityLogs: '',
78
+ observabilityMetrics: '',
79
+ observabilityTraces: '',
80
+ migrationPlan: '',
81
+ rollbackPlan: '',
82
+ needsOverride: false,
83
+ isExperimental: false,
84
+ aiConfidence: 0.8,
85
+ uncertaintyAreas: '',
86
+ complexityFactors: '',
87
+ });
88
+
89
+ fs.ensureDirSync(path.dirname(workingSpecPath));
90
+ fs.writeFileSync(workingSpecPath, specContent);
91
+ }
92
+
16
93
  /**
17
94
  * Get the last commit info for a branch
18
95
  * @param {string} branch - Branch name
@@ -53,6 +130,44 @@ function isBranchMerged(branch, target, root) {
53
130
  }
54
131
  }
55
132
 
133
+ /**
134
+ * Check if a branch has divergent commits from target (commits on branch not on target).
135
+ * @param {string} branch - Branch to check
136
+ * @param {string} target - Target branch (e.g., "main")
137
+ * @param {string} root - Repository root
138
+ * @returns {boolean}
139
+ */
140
+ function hasDivergentCommits(branch, target, root) {
141
+ try {
142
+ const count = execFileSync(
143
+ 'git',
144
+ ['rev-list', '--count', `${target}..${branch}`],
145
+ { cwd: root, encoding: 'utf8', stdio: 'pipe' }
146
+ ).trim();
147
+ return parseInt(count, 10) > 0;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Check if a worktree directory has dirty (uncommitted) files.
155
+ * @param {string} worktreePath - Path to the worktree
156
+ * @returns {boolean}
157
+ */
158
+ function hasDirtyFiles(worktreePath) {
159
+ try {
160
+ const status = execFileSync(
161
+ 'git',
162
+ ['status', '--porcelain'],
163
+ { cwd: worktreePath, encoding: 'utf8', stdio: 'pipe' }
164
+ ).trim();
165
+ return status.length > 0;
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
56
171
  /**
57
172
  * Get the canonical git repository root (main worktree, not a linked worktree).
58
173
  *
@@ -85,6 +200,11 @@ function getCurrentBranch() {
85
200
  }).trim();
86
201
  }
87
202
 
203
+ // Track whether we've already warned about schema violations this process.
204
+ // loadRegistry() is called multiple times per command; warning every time
205
+ // floods stderr and contributes to Claude Code context-window exhaustion.
206
+ let _schemaWarned = false;
207
+
88
208
  /**
89
209
  * Load the worktree registry
90
210
  * @param {string} root - Repository root
@@ -94,7 +214,21 @@ function loadRegistry(root) {
94
214
  const registryPath = path.join(root, REGISTRY_FILE);
95
215
  try {
96
216
  if (fs.existsSync(registryPath)) {
97
- return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
217
+ const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
218
+ try {
219
+ const validate = createValidator(getSchemaPath('worktrees.schema.json', root));
220
+ const result = validate(data);
221
+ if (!result.valid && !_schemaWarned) {
222
+ _schemaWarned = true;
223
+ console.warn('Worktree registry has schema violations:', result.errors);
224
+ }
225
+ } catch (schemaErr) {
226
+ if (!_schemaWarned) {
227
+ _schemaWarned = true;
228
+ console.warn('Could not validate worktree registry schema:', schemaErr.message);
229
+ }
230
+ }
231
+ return data;
98
232
  }
99
233
  } catch {
100
234
  // Corrupted registry, start fresh
@@ -108,6 +242,27 @@ function loadRegistry(root) {
108
242
  * @param {Object} registry - Registry object
109
243
  */
110
244
  function saveRegistry(root, registry) {
245
+ // Auto-prune destroyed entries whose branch and directory are both gone.
246
+ // This prevents the registry from accumulating ghost entries over time.
247
+ for (const [name, entry] of Object.entries(registry.worktrees || {})) {
248
+ if (entry.status !== 'destroyed') continue;
249
+ const dirGone = !fs.existsSync(entry.path);
250
+ let branchGone = true;
251
+ if (entry.branch) {
252
+ try {
253
+ execFileSync('git', ['rev-parse', '--verify', entry.branch], {
254
+ cwd: root, stdio: 'pipe',
255
+ });
256
+ branchGone = false;
257
+ } catch {
258
+ branchGone = true;
259
+ }
260
+ }
261
+ if (dirGone && branchGone) {
262
+ delete registry.worktrees[name];
263
+ }
264
+ }
265
+
111
266
  const registryPath = path.join(root, REGISTRY_FILE);
112
267
  fs.ensureDirSync(path.dirname(registryPath));
113
268
  fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
@@ -233,15 +388,64 @@ function createWorktree(name, options = {}) {
233
388
 
234
389
  const registry = loadRegistry(root);
235
390
 
236
- // Check for duplicate
391
+ // Check for duplicate in registry
237
392
  if (registry.worktrees[name]) {
238
- throw new Error(`Worktree '${name}' already exists. Use 'caws worktree destroy ${name}' first.`);
393
+ const existing = registry.worktrees[name];
394
+ if (existing.status !== 'destroyed') {
395
+ const ownerInfo = existing.owner ? ` (owned by session ${existing.owner})` : '';
396
+ throw new Error(
397
+ `Worktree '${name}' already exists with status '${existing.status}'${ownerInfo}.\n` +
398
+ `Use 'caws worktree destroy ${name}' first, or choose a different name.`
399
+ );
400
+ }
401
+ // Destroyed entries: check if another session owns the branch
402
+ if (existing.owner && existing.owner !== getAgentSessionId(root)) {
403
+ // Branch may still be in use by the owning session for merge
404
+ try {
405
+ const branchExists = execFileSync('git', ['rev-parse', '--verify', BRANCH_PREFIX + name], {
406
+ cwd: root, stdio: 'pipe',
407
+ }).toString().trim();
408
+ if (branchExists) {
409
+ throw new Error(
410
+ `Worktree '${name}' was destroyed but branch '${BRANCH_PREFIX}${name}' still exists ` +
411
+ `(owned by session ${existing.owner}).\n` +
412
+ `The owning session may still need this branch for merging.\n` +
413
+ `Choose a different name, or delete the branch first: git branch -d ${BRANCH_PREFIX}${name}`
414
+ );
415
+ }
416
+ } catch (e) {
417
+ if (e.message.includes('owned by session')) throw e;
418
+ // Branch doesn't exist — safe to reuse the name
419
+ }
420
+ }
239
421
  }
240
422
 
241
423
  const worktreePath = path.join(root, WORKTREES_DIR, name);
242
424
  const branchName = BRANCH_PREFIX + name;
243
425
  const base = baseBranch || getCurrentBranch();
244
426
 
427
+ // Check if the branch already exists in git (even if not in registry)
428
+ // This catches cases where another agent created the branch outside CAWS
429
+ try {
430
+ execFileSync('git', ['rev-parse', '--verify', branchName], {
431
+ cwd: root, stdio: 'pipe',
432
+ });
433
+ // Branch exists — refuse unless it's fully merged into base
434
+ const currentSession = getAgentSessionId(root);
435
+ const registryOwner = registry.worktrees[name]?.owner;
436
+ if (registryOwner && registryOwner !== currentSession) {
437
+ throw new Error(
438
+ `Branch '${branchName}' already exists and is owned by another session (${registryOwner}).\n` +
439
+ `Another agent may be using this branch. Choose a different worktree name.`
440
+ );
441
+ }
442
+ // Branch exists but no owner conflict — warn and reuse
443
+ console.warn(`Warning: Branch '${branchName}' already exists, reusing it.`);
444
+ } catch (e) {
445
+ if (e.message.includes('already exists and is owned')) throw e;
446
+ // Branch doesn't exist — this is the normal path
447
+ }
448
+
245
449
  // Create the worktree directory
246
450
  fs.ensureDirSync(path.dirname(worktreePath));
247
451
 
@@ -252,7 +456,7 @@ function createWorktree(name, options = {}) {
252
456
  stdio: 'pipe',
253
457
  });
254
458
  } catch (error) {
255
- // Branch might already exist
459
+ // Branch already exists (caught above and allowed) — attach to it
256
460
  if (error.message.includes('already exists')) {
257
461
  execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
258
462
  cwd: root,
@@ -306,46 +510,16 @@ function createWorktree(name, options = {}) {
306
510
  }
307
511
  }
308
512
 
309
- // Generate working spec if in standard+ mode and specId provided
513
+ // Materialize a worktree-local working spec. Prefer the canonical feature
514
+ // spec when it exists so isolated worktrees stay aligned with the main
515
+ // registry/resolver model.
310
516
  if (specId) {
311
517
  try {
312
- const { generateWorkingSpec } = require('../generators/working-spec');
313
- const specContent = generateWorkingSpec({
314
- projectId: specId,
315
- projectTitle: `Worktree: ${name}`,
316
- projectDescription: `Isolated worktree for ${name}`,
317
- riskTier: 3,
318
- projectMode: 'feature',
319
- scopeIn: scope || 'src/',
320
- scopeOut: 'node_modules/, dist/, build/',
321
- maxFiles: 25,
322
- maxLoc: 1000,
323
- blastModules: scope || 'src',
324
- dataMigration: false,
325
- rollbackSlo: '5m',
326
- projectThreats: '',
327
- projectInvariants: 'System maintains data consistency',
328
- acceptanceCriteria: 'Given current state, when action occurs, then expected result',
329
- a11yRequirements: 'keyboard',
330
- perfBudget: 250,
331
- securityRequirements: 'validation',
332
- contractType: '',
333
- contractPath: '',
334
- observabilityLogs: '',
335
- observabilityMetrics: '',
336
- observabilityTraces: '',
337
- migrationPlan: '',
338
- rollbackPlan: '',
339
- needsOverride: false,
340
- isExperimental: false,
341
- aiConfidence: 0.8,
342
- uncertaintyAreas: '',
343
- complexityFactors: '',
344
- });
345
- const specPath = path.join(cawsDest, 'working-spec.yaml');
346
- fs.ensureDirSync(path.dirname(specPath));
347
- fs.writeFileSync(specPath, specContent);
348
- } catch {
518
+ materializeWorktreeSpec(root, cawsDest, specId, name, scope);
519
+ } catch (error) {
520
+ console.warn(
521
+ chalk.yellow(`Could not materialize spec '${specId}' for worktree '${name}': ${error.message}`)
522
+ );
349
523
  // Non-fatal: spec generation is optional
350
524
  }
351
525
  }
@@ -358,9 +532,9 @@ function createWorktree(name, options = {}) {
358
532
  baseBranch: base,
359
533
  scope: scope || null,
360
534
  specId: specId || null,
361
- owner: options.owner || process.env.CLAUDE_SESSION_ID || null,
535
+ owner: options.owner || getAgentSessionId(root) || null,
362
536
  createdAt: new Date().toISOString(),
363
- status: 'active',
537
+ status: 'fresh',
364
538
  };
365
539
 
366
540
  registry.worktrees[name] = entry;
@@ -415,26 +589,44 @@ function reconcileRegistry(root) {
415
589
  (wt) => path.resolve(wt) === path.resolve(entry.path)
416
590
  );
417
591
 
592
+ const merged = entry.branch && entry.baseBranch
593
+ ? isBranchMerged(entry.branch, entry.baseBranch, root)
594
+ : false;
595
+ const divergent = entry.branch && entry.baseBranch
596
+ ? hasDivergentCommits(entry.branch, entry.baseBranch, root)
597
+ : false;
598
+ const dirty = exists ? hasDirtyFiles(entry.path) : false;
599
+
418
600
  let status;
419
601
  if (entry.status === 'destroyed') {
420
602
  status = 'destroyed';
421
603
  } else if (exists && inGit) {
422
- status = 'active';
604
+ // Worktree directory exists and is tracked by git
605
+ if (divergent || dirty) {
606
+ // Has commits beyond base or uncommitted work → active
607
+ status = 'active';
608
+ } else if (merged) {
609
+ // No divergent commits, branch aligned with base.
610
+ // Use stored status as history to distinguish fresh vs merged:
611
+ // - stored 'fresh' → never had divergent commits → still fresh
612
+ // - stored 'active' → had work that's now merged → merged
613
+ if (entry.status === 'active') {
614
+ status = 'merged';
615
+ } else {
616
+ status = 'fresh';
617
+ }
618
+ } else {
619
+ status = 'fresh';
620
+ }
423
621
  } else if (exists) {
424
622
  status = 'orphaned';
425
623
  } else {
426
- const merged = entry.branch && entry.baseBranch
427
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
428
- : false;
429
624
  status = merged ? 'stale-merged' : 'missing';
430
625
  }
431
626
 
432
627
  const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
433
- const merged = entry.branch && entry.baseBranch
434
- ? isBranchMerged(entry.branch, entry.baseBranch, root)
435
- : false;
436
628
 
437
- return { ...entry, status, lastCommit, merged };
629
+ return { ...entry, status, lastCommit, merged, divergent, dirty };
438
630
  });
439
631
 
440
632
  // Append unregistered worktrees discovered from git
@@ -466,15 +658,17 @@ function reconcileRegistry(root) {
466
658
  * prunes stale entries. Reports the delta before persisting.
467
659
  *
468
660
  * @param {Object} options
469
- * @param {boolean} [options.prune=false] - Remove destroyed and stale-merged entries
661
+ * @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
470
662
  * @param {boolean} [options.dryRun=false] - Report only, do not persist
663
+ * @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
471
664
  * @returns {{ repaired: Array, pruned: Array, skipped: Array }}
472
665
  */
473
666
  function repairWorktrees(options = {}) {
474
- const { prune: shouldPrune = false, dryRun = false } = options;
667
+ const { prune: shouldPrune = false, dryRun = false, force = false } = options;
475
668
  const root = getRepoRoot();
476
669
  const registry = loadRegistry(root);
477
670
  const { entries } = reconcileRegistry(root);
671
+ const currentSession = getAgentSessionId(root);
478
672
 
479
673
  const repaired = [];
480
674
  const pruned = [];
@@ -494,18 +688,47 @@ function repairWorktrees(options = {}) {
494
688
  if (!regEntry) continue;
495
689
 
496
690
  // Update registry status to match filesystem reality
497
- if (regEntry.status === 'active' && (entry.status === 'missing' || entry.status === 'stale-merged')) {
498
- repaired.push({ name: entry.name, action: 'status-updated', from: 'active', to: entry.status });
691
+ const wasAlive = regEntry.status === 'active' || regEntry.status === 'fresh';
692
+ const nowDead = entry.status === 'missing' || entry.status === 'stale-merged';
693
+ if (wasAlive && nowDead) {
694
+ repaired.push({
695
+ name: entry.name,
696
+ action: 'status-updated',
697
+ from: regEntry.status,
698
+ to: entry.status,
699
+ owner: entry.owner || null,
700
+ });
499
701
  }
500
702
 
501
- // Prune if requested and entry is dead
502
- if (shouldPrune && (entry.status === 'destroyed' || entry.status === 'stale-merged')) {
503
- if (!dryRun) {
504
- delete registry.worktrees[entry.name];
703
+ // Determine if entry is prunable (destroyed, stale-merged, or missing)
704
+ const isPrunable = entry.status === 'destroyed' ||
705
+ entry.status === 'stale-merged' ||
706
+ entry.status === 'missing';
707
+
708
+ if (!isPrunable) continue;
709
+
710
+ // Ownership check: refuse to prune another session's entries without --force
711
+ const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
712
+
713
+ if (shouldPrune && isPrunable) {
714
+ if (isOwnedByOther && !force) {
715
+ skipped.push({
716
+ name: entry.name,
717
+ reason: `owned by another session (${entry.owner}). Use --force to override`,
718
+ owner: entry.owner,
719
+ });
720
+ } else {
721
+ if (!dryRun) {
722
+ delete registry.worktrees[entry.name];
723
+ }
724
+ pruned.push({ name: entry.name, status: entry.status, owner: entry.owner || null });
505
725
  }
506
- pruned.push({ name: entry.name, status: entry.status });
507
- } else if (!shouldPrune && (entry.status === 'destroyed' || entry.status === 'stale-merged')) {
508
- skipped.push({ name: entry.name, reason: entry.status + ' (use --prune to remove)' });
726
+ } else if (!shouldPrune && isPrunable) {
727
+ skipped.push({
728
+ name: entry.name,
729
+ reason: entry.status + ' (use --prune to remove)',
730
+ owner: entry.owner || null,
731
+ });
509
732
  }
510
733
  }
511
734
 
@@ -524,11 +747,29 @@ function repairWorktrees(options = {}) {
524
747
  /**
525
748
  * List all registered worktrees with filesystem validation.
526
749
  * Delegates to reconcileRegistry() for state classification.
750
+ * Persists status transitions (fresh → active, active → merged) so
751
+ * future calls can distinguish "never had work" from "work was merged back".
527
752
  * @returns {Array} Worktree entries with status
528
753
  */
529
754
  function listWorktrees() {
530
755
  const root = getRepoRoot();
756
+ const registry = loadRegistry(root);
531
757
  const { entries } = reconcileRegistry(root);
758
+
759
+ // Persist status transitions so future reconcile can use stored status as history
760
+ let dirty = false;
761
+ for (const entry of entries) {
762
+ const regEntry = registry.worktrees[entry.name];
763
+ if (regEntry && regEntry.status !== entry.status &&
764
+ entry.status !== 'unregistered') {
765
+ regEntry.status = entry.status;
766
+ dirty = true;
767
+ }
768
+ }
769
+ if (dirty) {
770
+ saveRegistry(root, registry);
771
+ }
772
+
532
773
  return entries;
533
774
  }
534
775
 
@@ -541,6 +782,9 @@ function listWorktrees() {
541
782
  */
542
783
  function destroyWorktree(name, options = {}) {
543
784
  const root = getRepoRoot();
785
+ // Ensure CWD is not inside the worktree we're about to destroy.
786
+ // If CWD is the worktree directory, removing it crashes subsequent commands.
787
+ try { process.chdir(root); } catch { /* non-fatal */ }
544
788
  const registry = loadRegistry(root);
545
789
  const { deleteBranch = false, force = false } = options;
546
790
 
@@ -557,11 +801,12 @@ function destroyWorktree(name, options = {}) {
557
801
  }
558
802
  }
559
803
 
560
- // Ownership check: refuse to destroy another agent's active worktree without --force
561
- const currentSession = process.env.CLAUDE_SESSION_ID || null;
804
+ // Ownership check: refuse to destroy another agent's worktree without --force
805
+ const currentSession = getAgentSessionId(root);
806
+ const isLiveStatus = entry.status === 'active' || entry.status === 'fresh' || entry.status === 'merged';
562
807
  if (
563
808
  !force &&
564
- entry.status === 'active' &&
809
+ isLiveStatus &&
565
810
  entry.owner &&
566
811
  currentSession &&
567
812
  entry.owner !== currentSession
@@ -580,7 +825,7 @@ function destroyWorktree(name, options = {}) {
580
825
  // Even with --force, warn loudly when destroying another session's worktree
581
826
  if (
582
827
  force &&
583
- entry.status === 'active' &&
828
+ isLiveStatus &&
584
829
  entry.owner &&
585
830
  currentSession &&
586
831
  entry.owner !== currentSession
@@ -682,17 +927,32 @@ function mergeWorktree(name, options = {}) {
682
927
 
683
928
  const baseBranch = entry.baseBranch || 'main';
684
929
 
685
- // Check for uncommitted work in the worktree
930
+ // Check for uncommitted work in the worktree.
931
+ // Ignore .caws/ changes (provenance chain, registry) — these are
932
+ // infrastructure artifacts written by git hooks, not user work.
933
+ // The post-commit hook appends to .caws/provenance/chain.json after
934
+ // every commit, which immediately dirties the tree and blocks merges.
686
935
  if (fs.existsSync(entry.path)) {
687
936
  try {
688
- const status = execFileSync(
937
+ const rawStatus = execFileSync(
689
938
  'git',
690
939
  ['status', '--porcelain'],
691
940
  { cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
692
- ).trim();
693
- if (status) {
941
+ );
942
+ // Filter out .caws/ infrastructure changes (provenance, registry).
943
+ // Git porcelain format: "XY PATH" — 2 status chars, space, path.
944
+ // IMPORTANT: do NOT .trim() the raw output — it strips the leading
945
+ // space from " M file" (unstaged), corrupting the XY prefix and
946
+ // breaking substring(3) path extraction.
947
+ const statusLines = rawStatus.split('\n').filter(l => l.length > 0);
948
+ const userChanges = statusLines
949
+ .filter(line => {
950
+ const filePath = line.substring(3);
951
+ return !filePath.startsWith('.caws/');
952
+ }).join('\n');
953
+ if (userChanges) {
694
954
  throw new Error(
695
- `Worktree '${name}' has uncommitted changes:\n${status}\n` +
955
+ `Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
696
956
  `Commit or discard changes before merging.`
697
957
  );
698
958
  }
@@ -736,32 +996,54 @@ function mergeWorktree(name, options = {}) {
736
996
  };
737
997
  }
738
998
 
999
+ // Emit merge:pre event
1000
+ try {
1001
+ lifecycle.emit(EVENTS.MERGE_PRE, {
1002
+ worktreeName: name, branch: entry.branch, baseBranch, conflicts,
1003
+ timestamp: new Date().toISOString(),
1004
+ });
1005
+ } catch { /* non-fatal */ }
1006
+
1007
+ // Ensure CWD is the repo root BEFORE destroying the worktree.
1008
+ // If the caller's CWD is inside the worktree directory, destroying it
1009
+ // removes the CWD out from under the process, causing all subsequent
1010
+ // git commands to fail with "Unable to read current working directory".
1011
+ try { process.chdir(root); } catch { /* non-fatal */ }
1012
+
739
1013
  // Destroy the worktree (auto-forces since we're about to merge)
740
1014
  destroyWorktree(name, { deleteBranch: false, force: true });
741
1015
 
742
- // Switch to base branch
743
- const currentBranch = getCurrentBranch();
1016
+ // Switch to base branch (use cwd: root since getCurrentBranch has no cwd param)
1017
+ const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
1018
+ cwd: root, encoding: 'utf8', stdio: 'pipe',
1019
+ }).trim();
744
1020
  if (currentBranch !== baseBranch) {
745
1021
  execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
746
1022
  }
747
1023
 
748
1024
  // Merge
1025
+ // Use --no-verify to skip pre-commit/commit-msg hooks during merge.
1026
+ // The worktree commits were already validated by those hooks when originally
1027
+ // committed. Re-running them here adds seconds of blocking time (especially
1028
+ // in projects with heavy hooks like quality gates, YAML validation, etc.)
1029
+ // and can trigger OAuth token expiry races in long-running sessions.
749
1030
  const mergeMessage = message || `merge(worktree): ${name}`;
750
1031
  try {
751
1032
  execFileSync(
752
1033
  'git',
753
- ['merge', '--no-ff', entry.branch, '-m', mergeMessage],
1034
+ ['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
754
1035
  { cwd: root, stdio: 'pipe' }
755
1036
  );
756
1037
  } catch (error) {
757
- return {
758
- name,
759
- branch: entry.branch,
760
- baseBranch,
761
- merged: false,
1038
+ const failResult = {
1039
+ name, branch: entry.branch, baseBranch, merged: false,
762
1040
  conflicts: [`Merge failed: ${error.message}`],
763
1041
  message: 'Merge conflicts detected. Resolve with git and commit.',
764
1042
  };
1043
+ try {
1044
+ lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
1045
+ } catch { /* non-fatal */ }
1046
+ return failResult;
765
1047
  }
766
1048
 
767
1049
  // Delete branch after successful merge
@@ -773,13 +1055,11 @@ function mergeWorktree(name, options = {}) {
773
1055
  }
774
1056
  }
775
1057
 
776
- return {
777
- name,
778
- branch: entry.branch,
779
- baseBranch,
780
- merged: true,
781
- conflicts: [],
782
- };
1058
+ const mergeResult = { name, branch: entry.branch, baseBranch, merged: true, conflicts: [] };
1059
+ try {
1060
+ lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
1061
+ } catch { /* non-fatal */ }
1062
+ return mergeResult;
783
1063
  }
784
1064
 
785
1065
  /**
@@ -787,12 +1067,14 @@ function mergeWorktree(name, options = {}) {
787
1067
  * @param {Object} options - Prune options
788
1068
  * @param {number} [options.maxAgeDays] - Remove entries older than this many days
789
1069
  * @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
790
- * @returns {Array} Pruned entries
1070
+ * @param {boolean} [options.force] - Allow pruning entries owned by other sessions
1071
+ * @returns {{ pruned: Array, skipped: Array }} Pruned and skipped entries
791
1072
  */
792
1073
  function pruneWorktrees(options = {}) {
793
1074
  const root = getRepoRoot();
794
1075
  const registry = loadRegistry(root);
795
- const { maxAgeDays = 30, recentCommitMinutes = 60 } = options;
1076
+ const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
1077
+ const currentSession = getAgentSessionId(root);
796
1078
 
797
1079
  const now = new Date();
798
1080
  const pruned = [];
@@ -806,14 +1088,25 @@ function pruneWorktrees(options = {}) {
806
1088
  const shouldPrune =
807
1089
  // Always prune destroyed entries
808
1090
  entry.status === 'destroyed' ||
809
- // Prune active entries whose directory is gone (filesystem-registry desync)
810
- (entry.status === 'active' && !dirExists) ||
1091
+ // Prune active/fresh entries whose directory is gone (filesystem-registry desync)
1092
+ ((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
811
1093
  // Prune old missing entries
812
1094
  (!dirExists && ageDays > maxAgeDays);
813
1095
 
814
1096
  if (shouldPrune) {
815
- // Before pruning a non-destroyed entry, check for recent commits
816
- if (entry.status !== 'destroyed' && entry.branch) {
1097
+ // Ownership check: skip entries owned by other sessions unless --force
1098
+ const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
1099
+ if (isOwnedByOther && entry.status !== 'destroyed' && !force) {
1100
+ skipped.push({
1101
+ name,
1102
+ reason: `owned by another session (${entry.owner})`,
1103
+ entry,
1104
+ });
1105
+ continue;
1106
+ }
1107
+
1108
+ // Before pruning a non-destroyed entry, check for recent commits (skip if --force)
1109
+ if (!force && entry.status !== 'destroyed' && entry.branch) {
817
1110
  const lastCommit = getLastCommitInfo(entry.branch, root);
818
1111
  if (lastCommit) {
819
1112
  const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
@@ -863,9 +1156,13 @@ module.exports = {
863
1156
  getRepoRoot,
864
1157
  getLastCommitInfo,
865
1158
  isBranchMerged,
1159
+ hasDivergentCommits,
1160
+ hasDirtyFiles,
866
1161
  discoverUnregisteredWorktrees,
867
1162
  autoRegisterWorktree,
868
1163
  WORKTREES_DIR,
869
1164
  REGISTRY_FILE,
870
1165
  BRANCH_PREFIX,
1166
+ findFeatureSpecPath,
1167
+ materializeWorktreeSpec,
871
1168
  };