@paths.design/caws-cli 9.3.0 → 9.3.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.
@@ -39,7 +39,7 @@ function detectTestRunner(projectRoot) {
39
39
  if (fs.existsSync(fp)) {
40
40
  try {
41
41
  if (fs.readFileSync(fp, 'utf8').includes(needle)) return check.runner;
42
- } catch (_) {}
42
+ } catch (_) { /* ignore unreadable config */ }
43
43
  }
44
44
  }
45
45
  }
@@ -123,7 +123,7 @@ function runNodeid(nodeid, runner, projectRoot) {
123
123
  try {
124
124
  switch (runner) {
125
125
  case 'pytest': {
126
- const out = execFileSync('python3', ['-m', 'pytest', '-x', '--tb=short', nodeid], {
126
+ execFileSync('python3', ['-m', 'pytest', '-x', '--tb=short', nodeid], {
127
127
  cwd: projectRoot, encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'],
128
128
  });
129
129
  return { passed: true, detail: 'tests passed' };
@@ -215,7 +215,7 @@ function checkEvidence(evidence, projectRoot) {
215
215
  if (files) {
216
216
  return { found: true, detail: `found: ${files.split('\n')[0]}` };
217
217
  }
218
- } catch (_) {}
218
+ } catch (_) { /* ignore unreadable config */ }
219
219
  }
220
220
  }
221
221
 
@@ -350,7 +350,7 @@ function loadSpecs(projectRoot, targetSpecId) {
350
350
  if (s && !TERMINAL_STATUSES.has(s.status)) {
351
351
  specs.push({ path: workingSpec, spec: s });
352
352
  }
353
- } catch (_) {}
353
+ } catch (_) { /* ignore unreadable config */ }
354
354
  }
355
355
 
356
356
  if (fs.existsSync(specsDir)) {
@@ -360,7 +360,7 @@ function loadSpecs(projectRoot, targetSpecId) {
360
360
  if (s && !TERMINAL_STATUSES.has(s.status)) {
361
361
  specs.push({ path: path.join(specsDir, f), spec: s });
362
362
  }
363
- } catch (_) {}
363
+ } catch (_) { /* ignore unreadable config */ }
364
364
  }
365
365
  }
366
366
 
@@ -203,17 +203,8 @@ function detectCAWSSetup(cwd = process.cwd()) {
203
203
  * @returns {string} Project root directory path
204
204
  */
205
205
  function findProjectRoot(startDir = process.cwd()) {
206
- // Walk up looking for .caws/ directory
207
- let dir = path.resolve(startDir);
208
- const root = path.parse(dir).root;
209
- while (dir !== root) {
210
- if (fs.existsSync(path.join(dir, '.caws'))) {
211
- return dir;
212
- }
213
- dir = path.dirname(dir);
214
- }
215
-
216
- // Fallback: try git root
206
+ // In a monorepo, nested packages may have their own .caws/ (scaffold debris).
207
+ // The git root's .caws/ is authoritative — check it first.
217
208
  try {
218
209
  const { execFileSync } = require('child_process');
219
210
  const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
@@ -225,7 +216,17 @@ function findProjectRoot(startDir = process.cwd()) {
225
216
  return gitRoot;
226
217
  }
227
218
  } catch {
228
- // Not a git repo or git not available
219
+ // Not a git repo or git not available — fall through
220
+ }
221
+
222
+ // Walk up looking for .caws/ directory (non-git projects)
223
+ let dir = path.resolve(startDir);
224
+ const root = path.parse(dir).root;
225
+ while (dir !== root) {
226
+ if (fs.existsSync(path.join(dir, '.caws'))) {
227
+ return dir;
228
+ }
229
+ dir = path.dirname(dir);
229
230
  }
230
231
 
231
232
  // Final fallback: cwd
@@ -101,6 +101,106 @@ function saveRegistry(root, registry) {
101
101
  fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
102
102
  }
103
103
 
104
+ /**
105
+ * Discover git worktrees under .caws/worktrees/ that are not in the registry.
106
+ * @param {string} root - Repository root
107
+ * @param {Object} registry - Current registry object
108
+ * @returns {Array<{ name: string, path: string, branch: string }>}
109
+ */
110
+ function discoverUnregisteredWorktrees(root, registry) {
111
+ const unregistered = [];
112
+ try {
113
+ const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
114
+ cwd: root,
115
+ encoding: 'utf8',
116
+ stdio: 'pipe',
117
+ });
118
+ let worktreesDir;
119
+ try {
120
+ worktreesDir = fs.realpathSync(path.resolve(root, WORKTREES_DIR));
121
+ } catch {
122
+ // Directory might not exist yet
123
+ worktreesDir = path.resolve(root, WORKTREES_DIR);
124
+ }
125
+
126
+ const blocks = output.split('\n\n').filter(Boolean);
127
+ for (const block of blocks) {
128
+ const lines = block.split('\n');
129
+ const wtLine = lines.find((l) => l.startsWith('worktree '));
130
+ const branchLine = lines.find((l) => l.startsWith('branch '));
131
+ if (!wtLine) continue;
132
+
133
+ const wtPath = wtLine.replace('worktree ', '');
134
+ let resolvedPath;
135
+ try {
136
+ resolvedPath = fs.realpathSync(wtPath);
137
+ } catch {
138
+ resolvedPath = path.resolve(wtPath);
139
+ }
140
+
141
+ // Only consider worktrees under .caws/worktrees/
142
+ if (!resolvedPath.startsWith(worktreesDir + path.sep)) continue;
143
+
144
+ const name = path.basename(resolvedPath);
145
+ if (registry.worktrees[name]) continue;
146
+
147
+ const branch = branchLine
148
+ ? branchLine.replace('branch refs/heads/', '')
149
+ : `${BRANCH_PREFIX}${name}`;
150
+ unregistered.push({ name, path: resolvedPath, branch });
151
+ }
152
+ } catch {
153
+ // git worktree list failed
154
+ }
155
+ return unregistered;
156
+ }
157
+
158
+ /**
159
+ * Auto-register an unregistered worktree. Infers baseBranch via merge-base.
160
+ * @param {string} root - Repository root
161
+ * @param {Object} registry - Registry object (mutated in place)
162
+ * @param {{ name: string, path: string, branch: string }} discovered
163
+ * @returns {Object} The registered entry
164
+ */
165
+ function autoRegisterWorktree(root, registry, discovered) {
166
+ let baseBranch = 'main';
167
+ try {
168
+ execFileSync(
169
+ 'git',
170
+ ['merge-base', discovered.branch, 'main'],
171
+ { cwd: root, encoding: 'utf8', stdio: 'pipe' }
172
+ );
173
+ } catch {
174
+ try {
175
+ execFileSync(
176
+ 'git',
177
+ ['merge-base', discovered.branch, 'master'],
178
+ { cwd: root, encoding: 'utf8', stdio: 'pipe' }
179
+ );
180
+ baseBranch = 'master';
181
+ } catch {
182
+ // Keep 'main' as default
183
+ }
184
+ }
185
+
186
+ const entry = {
187
+ name: discovered.name,
188
+ path: discovered.path,
189
+ branch: discovered.branch,
190
+ baseBranch,
191
+ scope: null,
192
+ specId: null,
193
+ owner: null,
194
+ createdAt: new Date().toISOString(),
195
+ status: 'active',
196
+ autoRegistered: true,
197
+ };
198
+
199
+ registry.worktrees[discovered.name] = entry;
200
+ saveRegistry(root, registry);
201
+ return entry;
202
+ }
203
+
104
204
  /**
105
205
  * Create a new git worktree with scope isolation
106
206
  * @param {string} name - Worktree name
@@ -308,6 +408,25 @@ function listWorktrees() {
308
408
  };
309
409
  });
310
410
 
411
+ // Append unregistered worktrees discovered from git
412
+ const unregistered = discoverUnregisteredWorktrees(root, registry);
413
+ for (const discovered of unregistered) {
414
+ const lastCommit = getLastCommitInfo(discovered.branch, root);
415
+ entries.push({
416
+ name: discovered.name,
417
+ path: discovered.path,
418
+ branch: discovered.branch,
419
+ baseBranch: null,
420
+ scope: null,
421
+ specId: null,
422
+ owner: null,
423
+ createdAt: null,
424
+ status: 'unregistered',
425
+ lastCommit,
426
+ merged: false,
427
+ });
428
+ }
429
+
311
430
  return entries;
312
431
  }
313
432
 
@@ -323,9 +442,17 @@ function destroyWorktree(name, options = {}) {
323
442
  const registry = loadRegistry(root);
324
443
  const { deleteBranch = false, force = false } = options;
325
444
 
326
- const entry = registry.worktrees[name];
445
+ let entry = registry.worktrees[name];
327
446
  if (!entry) {
328
- throw new Error(`Worktree '${name}' not found in registry`);
447
+ // Fallback: scan git for unregistered worktree and auto-register
448
+ const unregistered = discoverUnregisteredWorktrees(root, registry);
449
+ const discovered = unregistered.find((u) => u.name === name);
450
+ if (discovered) {
451
+ console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
452
+ entry = autoRegisterWorktree(root, registry, discovered);
453
+ } else {
454
+ throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
455
+ }
329
456
  }
330
457
 
331
458
  // Ownership check: refuse to destroy another agent's active worktree without --force
@@ -438,9 +565,17 @@ function mergeWorktree(name, options = {}) {
438
565
  const registry = loadRegistry(root);
439
566
  const { dryRun = false, deleteBranch = true, message } = options;
440
567
 
441
- const entry = registry.worktrees[name];
568
+ let entry = registry.worktrees[name];
442
569
  if (!entry) {
443
- throw new Error(`Worktree '${name}' not found in registry`);
570
+ // Fallback: scan git for unregistered worktree and auto-register
571
+ const unregistered = discoverUnregisteredWorktrees(root, registry);
572
+ const discovered = unregistered.find((u) => u.name === name);
573
+ if (discovered) {
574
+ console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
575
+ entry = autoRegisterWorktree(root, registry, discovered);
576
+ } else {
577
+ throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
578
+ }
444
579
  }
445
580
 
446
581
  const baseBranch = entry.baseBranch || 'main';
@@ -469,7 +604,7 @@ function mergeWorktree(name, options = {}) {
469
604
  let conflicts = [];
470
605
  try {
471
606
  // New-style merge-tree: takes two branches, computes merge-base automatically
472
- const mergeTreeResult = execFileSync(
607
+ execFileSync(
473
608
  'git',
474
609
  ['merge-tree', '--write-tree', baseBranch, entry.branch],
475
610
  { cwd: root, encoding: 'utf8', stdio: 'pipe' }
@@ -624,6 +759,8 @@ module.exports = {
624
759
  getRepoRoot,
625
760
  getLastCommitInfo,
626
761
  isBranchMerged,
762
+ discoverUnregisteredWorktrees,
763
+ autoRegisterWorktree,
627
764
  WORKTREES_DIR,
628
765
  REGISTRY_FILE,
629
766
  BRANCH_PREFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "9.3.0",
3
+ "version": "9.3.1",
4
4
  "description": "CAWS CLI - Coding Agent Workflow System command-line tools for spec management, quality gates, and AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {