@paths.design/caws-cli 9.3.1 → 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 +104 -26
  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 +136 -150
  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 +36 -23
  61. package/dist/templates/.claude/hooks/worktree-write-guard.sh +6 -5
  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 +496 -95
  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 +36 -23
  113. package/templates/.claude/hooks/worktree-write-guard.sh +6 -5
  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
@@ -54,13 +131,63 @@ function isBranchMerged(branch, target, root) {
54
131
  }
55
132
 
56
133
  /**
57
- * Get the git repository root
58
- * @returns {string} Absolute path to repo root
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
+
171
+ /**
172
+ * Get the canonical git repository root (main worktree, not a linked worktree).
173
+ *
174
+ * `git rev-parse --show-toplevel` returns the root of whichever worktree
175
+ * the CWD is inside. In a linked worktree that is NOT the main repo root,
176
+ * so CAWS would read the wrong (or missing) .caws/worktrees.json.
177
+ *
178
+ * `--git-common-dir` always resolves to the main repo's .git directory,
179
+ * even from inside a linked worktree. Its parent is the canonical repo root.
180
+ *
181
+ * @returns {string} Absolute path to the main repo root
59
182
  */
60
183
  function getRepoRoot() {
61
- return execFileSync('git', ['rev-parse', '--show-toplevel'], {
62
- encoding: 'utf8',
63
- }).trim();
184
+ const gitCommonDir = execFileSync(
185
+ 'git',
186
+ ['rev-parse', '--path-format=absolute', '--git-common-dir'],
187
+ { encoding: 'utf8' }
188
+ ).trim();
189
+ // gitCommonDir is /path/to/main-repo/.git — parent is the repo root
190
+ return path.dirname(gitCommonDir);
64
191
  }
65
192
 
66
193
  /**
@@ -73,6 +200,11 @@ function getCurrentBranch() {
73
200
  }).trim();
74
201
  }
75
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
+
76
208
  /**
77
209
  * Load the worktree registry
78
210
  * @param {string} root - Repository root
@@ -82,7 +214,21 @@ function loadRegistry(root) {
82
214
  const registryPath = path.join(root, REGISTRY_FILE);
83
215
  try {
84
216
  if (fs.existsSync(registryPath)) {
85
- 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;
86
232
  }
87
233
  } catch {
88
234
  // Corrupted registry, start fresh
@@ -96,6 +242,27 @@ function loadRegistry(root) {
96
242
  * @param {Object} registry - Registry object
97
243
  */
98
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
+
99
266
  const registryPath = path.join(root, REGISTRY_FILE);
100
267
  fs.ensureDirSync(path.dirname(registryPath));
101
268
  fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
@@ -221,15 +388,64 @@ function createWorktree(name, options = {}) {
221
388
 
222
389
  const registry = loadRegistry(root);
223
390
 
224
- // Check for duplicate
391
+ // Check for duplicate in registry
225
392
  if (registry.worktrees[name]) {
226
- 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
+ }
227
421
  }
228
422
 
229
423
  const worktreePath = path.join(root, WORKTREES_DIR, name);
230
424
  const branchName = BRANCH_PREFIX + name;
231
425
  const base = baseBranch || getCurrentBranch();
232
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
+
233
449
  // Create the worktree directory
234
450
  fs.ensureDirSync(path.dirname(worktreePath));
235
451
 
@@ -240,7 +456,7 @@ function createWorktree(name, options = {}) {
240
456
  stdio: 'pipe',
241
457
  });
242
458
  } catch (error) {
243
- // Branch might already exist
459
+ // Branch already exists (caught above and allowed) — attach to it
244
460
  if (error.message.includes('already exists')) {
245
461
  execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
246
462
  cwd: root,
@@ -294,46 +510,16 @@ function createWorktree(name, options = {}) {
294
510
  }
295
511
  }
296
512
 
297
- // 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.
298
516
  if (specId) {
299
517
  try {
300
- const { generateWorkingSpec } = require('../generators/working-spec');
301
- const specContent = generateWorkingSpec({
302
- projectId: specId,
303
- projectTitle: `Worktree: ${name}`,
304
- projectDescription: `Isolated worktree for ${name}`,
305
- riskTier: 3,
306
- projectMode: 'feature',
307
- scopeIn: scope || 'src/',
308
- scopeOut: 'node_modules/, dist/, build/',
309
- maxFiles: 25,
310
- maxLoc: 1000,
311
- blastModules: scope || 'src',
312
- dataMigration: false,
313
- rollbackSlo: '5m',
314
- projectThreats: '',
315
- projectInvariants: 'System maintains data consistency',
316
- acceptanceCriteria: 'Given current state, when action occurs, then expected result',
317
- a11yRequirements: 'keyboard',
318
- perfBudget: 250,
319
- securityRequirements: 'validation',
320
- contractType: '',
321
- contractPath: '',
322
- observabilityLogs: '',
323
- observabilityMetrics: '',
324
- observabilityTraces: '',
325
- migrationPlan: '',
326
- rollbackPlan: '',
327
- needsOverride: false,
328
- isExperimental: false,
329
- aiConfidence: 0.8,
330
- uncertaintyAreas: '',
331
- complexityFactors: '',
332
- });
333
- const specPath = path.join(cawsDest, 'working-spec.yaml');
334
- fs.ensureDirSync(path.dirname(specPath));
335
- fs.writeFileSync(specPath, specContent);
336
- } 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
+ );
337
523
  // Non-fatal: spec generation is optional
338
524
  }
339
525
  }
@@ -346,9 +532,9 @@ function createWorktree(name, options = {}) {
346
532
  baseBranch: base,
347
533
  scope: scope || null,
348
534
  specId: specId || null,
349
- owner: options.owner || process.env.CLAUDE_SESSION_ID || null,
535
+ owner: options.owner || getAgentSessionId(root) || null,
350
536
  createdAt: new Date().toISOString(),
351
- status: 'active',
537
+ status: 'fresh',
352
538
  };
353
539
 
354
540
  registry.worktrees[name] = entry;
@@ -358,19 +544,31 @@ function createWorktree(name, options = {}) {
358
544
  }
359
545
 
360
546
  /**
361
- * List all registered worktrees with filesystem validation
362
- * @returns {Array} Worktree entries with status
547
+ * Reconcile registry state against git worktree list and filesystem.
548
+ *
549
+ * Non-destructive read that classifies every known worktree entry
550
+ * (from registry + git discovery) into one of:
551
+ * active — directory exists AND in git worktree list
552
+ * orphaned — directory exists but NOT in git worktree list
553
+ * missing — directory gone, branch may or may not exist
554
+ * destroyed — explicitly destroyed via CAWS
555
+ * unregistered — in git worktree list but not in registry
556
+ * stale-merged — missing + branch already merged to base
557
+ *
558
+ * Does NOT mutate the registry. Callers decide what to persist.
559
+ *
560
+ * @param {string} root - Repository root
561
+ * @returns {{ entries: Array, gitWorktrees: string[] }}
363
562
  */
364
- function listWorktrees() {
365
- const root = getRepoRoot();
563
+ function reconcileRegistry(root) {
366
564
  const registry = loadRegistry(root);
367
565
 
368
- // Get actual git worktrees for validation
369
566
  let gitWorktrees = [];
370
567
  try {
371
568
  const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
372
569
  cwd: root,
373
570
  encoding: 'utf8',
571
+ stdio: 'pipe',
374
572
  });
375
573
  gitWorktrees = output
376
574
  .split('\n\n')
@@ -390,22 +588,45 @@ function listWorktrees() {
390
588
  const inGit = gitWorktrees.some(
391
589
  (wt) => path.resolve(wt) === path.resolve(entry.path)
392
590
  );
393
- const status = exists && inGit ? 'active' : exists ? 'orphaned' : 'missing';
394
-
395
- // Enrich with commit recency
396
- const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
397
591
 
398
- // Check if branch is already merged to base
399
592
  const merged = entry.branch && entry.baseBranch
400
593
  ? isBranchMerged(entry.branch, entry.baseBranch, root)
401
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
+
600
+ let status;
601
+ if (entry.status === 'destroyed') {
602
+ status = 'destroyed';
603
+ } else if (exists && inGit) {
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
+ }
621
+ } else if (exists) {
622
+ status = 'orphaned';
623
+ } else {
624
+ status = merged ? 'stale-merged' : 'missing';
625
+ }
402
626
 
403
- return {
404
- ...entry,
405
- status,
406
- lastCommit,
407
- merged,
408
- };
627
+ const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
628
+
629
+ return { ...entry, status, lastCommit, merged, divergent, dirty };
409
630
  });
410
631
 
411
632
  // Append unregistered worktrees discovered from git
@@ -427,6 +648,128 @@ function listWorktrees() {
427
648
  });
428
649
  }
429
650
 
651
+ return { entries, gitWorktrees };
652
+ }
653
+
654
+ /**
655
+ * Repair registry drift caused by manual git operations outside CAWS.
656
+ *
657
+ * Scans registry vs git vs filesystem, classifies each entry, and optionally
658
+ * prunes stale entries. Reports the delta before persisting.
659
+ *
660
+ * @param {Object} options
661
+ * @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
662
+ * @param {boolean} [options.dryRun=false] - Report only, do not persist
663
+ * @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
664
+ * @returns {{ repaired: Array, pruned: Array, skipped: Array }}
665
+ */
666
+ function repairWorktrees(options = {}) {
667
+ const { prune: shouldPrune = false, dryRun = false, force = false } = options;
668
+ const root = getRepoRoot();
669
+ const registry = loadRegistry(root);
670
+ const { entries } = reconcileRegistry(root);
671
+ const currentSession = getAgentSessionId(root);
672
+
673
+ const repaired = [];
674
+ const pruned = [];
675
+ const skipped = [];
676
+
677
+ for (const entry of entries) {
678
+ const regEntry = registry.worktrees[entry.name];
679
+
680
+ if (entry.status === 'unregistered') {
681
+ if (!dryRun) {
682
+ autoRegisterWorktree(root, registry, entry);
683
+ }
684
+ repaired.push({ name: entry.name, action: 'registered', status: entry.status });
685
+ continue;
686
+ }
687
+
688
+ if (!regEntry) continue;
689
+
690
+ // Update registry status to match filesystem reality
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
+ });
701
+ }
702
+
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 });
725
+ }
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
+ });
732
+ }
733
+ }
734
+
735
+ if (!dryRun) {
736
+ saveRegistry(root, registry);
737
+ try {
738
+ execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
739
+ } catch {
740
+ // Non-fatal
741
+ }
742
+ }
743
+
744
+ return { repaired, pruned, skipped };
745
+ }
746
+
747
+ /**
748
+ * List all registered worktrees with filesystem validation.
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".
752
+ * @returns {Array} Worktree entries with status
753
+ */
754
+ function listWorktrees() {
755
+ const root = getRepoRoot();
756
+ const registry = loadRegistry(root);
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
+
430
773
  return entries;
431
774
  }
432
775
 
@@ -439,6 +782,9 @@ function listWorktrees() {
439
782
  */
440
783
  function destroyWorktree(name, options = {}) {
441
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 */ }
442
788
  const registry = loadRegistry(root);
443
789
  const { deleteBranch = false, force = false } = options;
444
790
 
@@ -455,11 +801,12 @@ function destroyWorktree(name, options = {}) {
455
801
  }
456
802
  }
457
803
 
458
- // Ownership check: refuse to destroy another agent's active worktree without --force
459
- 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';
460
807
  if (
461
808
  !force &&
462
- entry.status === 'active' &&
809
+ isLiveStatus &&
463
810
  entry.owner &&
464
811
  currentSession &&
465
812
  entry.owner !== currentSession
@@ -478,7 +825,7 @@ function destroyWorktree(name, options = {}) {
478
825
  // Even with --force, warn loudly when destroying another session's worktree
479
826
  if (
480
827
  force &&
481
- entry.status === 'active' &&
828
+ isLiveStatus &&
482
829
  entry.owner &&
483
830
  currentSession &&
484
831
  entry.owner !== currentSession
@@ -580,17 +927,32 @@ function mergeWorktree(name, options = {}) {
580
927
 
581
928
  const baseBranch = entry.baseBranch || 'main';
582
929
 
583
- // 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.
584
935
  if (fs.existsSync(entry.path)) {
585
936
  try {
586
- const status = execFileSync(
937
+ const rawStatus = execFileSync(
587
938
  'git',
588
939
  ['status', '--porcelain'],
589
940
  { cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
590
- ).trim();
591
- 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) {
592
954
  throw new Error(
593
- `Worktree '${name}' has uncommitted changes:\n${status}\n` +
955
+ `Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
594
956
  `Commit or discard changes before merging.`
595
957
  );
596
958
  }
@@ -634,32 +996,54 @@ function mergeWorktree(name, options = {}) {
634
996
  };
635
997
  }
636
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
+
637
1013
  // Destroy the worktree (auto-forces since we're about to merge)
638
1014
  destroyWorktree(name, { deleteBranch: false, force: true });
639
1015
 
640
- // Switch to base branch
641
- 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();
642
1020
  if (currentBranch !== baseBranch) {
643
1021
  execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
644
1022
  }
645
1023
 
646
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.
647
1030
  const mergeMessage = message || `merge(worktree): ${name}`;
648
1031
  try {
649
1032
  execFileSync(
650
1033
  'git',
651
- ['merge', '--no-ff', entry.branch, '-m', mergeMessage],
1034
+ ['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
652
1035
  { cwd: root, stdio: 'pipe' }
653
1036
  );
654
1037
  } catch (error) {
655
- return {
656
- name,
657
- branch: entry.branch,
658
- baseBranch,
659
- merged: false,
1038
+ const failResult = {
1039
+ name, branch: entry.branch, baseBranch, merged: false,
660
1040
  conflicts: [`Merge failed: ${error.message}`],
661
1041
  message: 'Merge conflicts detected. Resolve with git and commit.',
662
1042
  };
1043
+ try {
1044
+ lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
1045
+ } catch { /* non-fatal */ }
1046
+ return failResult;
663
1047
  }
664
1048
 
665
1049
  // Delete branch after successful merge
@@ -671,13 +1055,11 @@ function mergeWorktree(name, options = {}) {
671
1055
  }
672
1056
  }
673
1057
 
674
- return {
675
- name,
676
- branch: entry.branch,
677
- baseBranch,
678
- merged: true,
679
- conflicts: [],
680
- };
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;
681
1063
  }
682
1064
 
683
1065
  /**
@@ -685,12 +1067,14 @@ function mergeWorktree(name, options = {}) {
685
1067
  * @param {Object} options - Prune options
686
1068
  * @param {number} [options.maxAgeDays] - Remove entries older than this many days
687
1069
  * @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
688
- * @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
689
1072
  */
690
1073
  function pruneWorktrees(options = {}) {
691
1074
  const root = getRepoRoot();
692
1075
  const registry = loadRegistry(root);
693
- const { maxAgeDays = 30, recentCommitMinutes = 60 } = options;
1076
+ const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
1077
+ const currentSession = getAgentSessionId(root);
694
1078
 
695
1079
  const now = new Date();
696
1080
  const pruned = [];
@@ -704,14 +1088,25 @@ function pruneWorktrees(options = {}) {
704
1088
  const shouldPrune =
705
1089
  // Always prune destroyed entries
706
1090
  entry.status === 'destroyed' ||
707
- // Prune active entries whose directory is gone (filesystem-registry desync)
708
- (entry.status === 'active' && !dirExists) ||
1091
+ // Prune active/fresh entries whose directory is gone (filesystem-registry desync)
1092
+ ((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
709
1093
  // Prune old missing entries
710
1094
  (!dirExists && ageDays > maxAgeDays);
711
1095
 
712
1096
  if (shouldPrune) {
713
- // Before pruning a non-destroyed entry, check for recent commits
714
- 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) {
715
1110
  const lastCommit = getLastCommitInfo(entry.branch, root);
716
1111
  if (lastCommit) {
717
1112
  const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
@@ -755,13 +1150,19 @@ module.exports = {
755
1150
  destroyWorktree,
756
1151
  mergeWorktree,
757
1152
  pruneWorktrees,
1153
+ repairWorktrees,
1154
+ reconcileRegistry,
758
1155
  loadRegistry,
759
1156
  getRepoRoot,
760
1157
  getLastCommitInfo,
761
1158
  isBranchMerged,
1159
+ hasDivergentCommits,
1160
+ hasDirtyFiles,
762
1161
  discoverUnregisteredWorktrees,
763
1162
  autoRegisterWorktree,
764
1163
  WORKTREES_DIR,
765
1164
  REGISTRY_FILE,
766
1165
  BRANCH_PREFIX,
1166
+ findFeatureSpecPath,
1167
+ materializeWorktreeSpec,
767
1168
  };