@paths.design/caws-cli 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/dist/commands/archive.d.ts +1 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +185 -39
- package/dist/commands/mode.d.ts +2 -1
- package/dist/commands/mode.d.ts.map +1 -1
- package/dist/commands/provenance.d.ts.map +1 -1
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/worktree.d.ts +7 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +136 -0
- package/dist/config/lite-scope.d.ts +33 -0
- package/dist/config/lite-scope.d.ts.map +1 -0
- package/dist/config/lite-scope.js +158 -0
- package/dist/config/modes.d.ts +90 -51
- package/dist/config/modes.d.ts.map +1 -1
- package/dist/config/modes.js +26 -0
- package/dist/error-handler.d.ts +3 -16
- package/dist/error-handler.d.ts.map +1 -1
- package/dist/generators/jest-config-generator.d.ts +32 -0
- package/dist/generators/jest-config-generator.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/scaffold/claude-hooks.d.ts +28 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -0
- package/dist/scaffold/claude-hooks.js +28 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +90 -88
- package/dist/templates/.caws/schemas/scope.schema.json +52 -0
- package/dist/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/dist/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +33 -0
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +93 -6
- package/dist/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/dist/templates/.cursor/README.md +0 -3
- package/dist/templates/.github/copilot-instructions.md +82 -0
- package/dist/templates/.junie/guidelines.md +73 -0
- package/dist/templates/.vscode/launch.json +0 -27
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/dist/templates/CLAUDE.md +101 -0
- package/dist/templates/agents.md +73 -1016
- package/dist/templates/docs/README.md +5 -5
- package/dist/test-analysis.d.ts +50 -1
- package/dist/test-analysis.d.ts.map +1 -1
- package/dist/utils/error-categories.d.ts +52 -0
- package/dist/utils/error-categories.d.ts.map +1 -0
- package/dist/utils/gitignore-updater.d.ts +1 -1
- package/dist/utils/gitignore-updater.d.ts.map +1 -1
- package/dist/utils/gitignore-updater.js +4 -0
- package/dist/utils/ide-detection.js +133 -0
- package/dist/utils/quality-gates-utils.d.ts +49 -0
- package/dist/utils/quality-gates-utils.d.ts.map +1 -0
- package/dist/utils/typescript-detector.d.ts +8 -5
- package/dist/utils/typescript-detector.d.ts.map +1 -1
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.d.ts +54 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.js +378 -0
- package/package.json +5 -1
- package/templates/.caws/schemas/scope.schema.json +52 -0
- package/templates/.caws/schemas/working-spec.schema.json +1 -1
- package/templates/.caws/schemas/worktrees.schema.json +36 -0
- package/templates/.claude/hooks/block-dangerous.sh +33 -0
- package/templates/.claude/hooks/lite-sprawl-check.sh +117 -0
- package/templates/.claude/hooks/scope-guard.sh +93 -6
- package/templates/.claude/hooks/simplification-guard.sh +92 -0
- package/templates/.cursor/README.md +0 -3
- package/templates/.github/copilot-instructions.md +82 -0
- package/templates/.junie/guidelines.md +73 -0
- package/templates/.vscode/launch.json +0 -27
- package/templates/.windsurf/rules/caws-quality-standards.md +54 -0
- package/templates/AGENTS.md +104 -0
- package/templates/CLAUDE.md +101 -0
- package/templates/docs/README.md +5 -5
- package/templates/.github/copilot/instructions.md +0 -311
- package/templates/agents.md +0 -1047
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Git Worktree Manager
|
|
3
|
+
* Provides CRUD operations for git worktrees with scope isolation
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
const WORKTREES_DIR = '.caws/worktrees';
|
|
13
|
+
const REGISTRY_FILE = '.caws/worktrees.json';
|
|
14
|
+
const BRANCH_PREFIX = 'caws/';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the git repository root
|
|
18
|
+
* @returns {string} Absolute path to repo root
|
|
19
|
+
*/
|
|
20
|
+
function getRepoRoot() {
|
|
21
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
}).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get current branch name
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function getCurrentBranch() {
|
|
31
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the worktree registry
|
|
38
|
+
* @param {string} root - Repository root
|
|
39
|
+
* @returns {Object} Registry object
|
|
40
|
+
*/
|
|
41
|
+
function loadRegistry(root) {
|
|
42
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(registryPath)) {
|
|
45
|
+
return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Corrupted registry, start fresh
|
|
49
|
+
}
|
|
50
|
+
return { version: 1, worktrees: {} };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save the worktree registry
|
|
55
|
+
* @param {string} root - Repository root
|
|
56
|
+
* @param {Object} registry - Registry object
|
|
57
|
+
*/
|
|
58
|
+
function saveRegistry(root, registry) {
|
|
59
|
+
const registryPath = path.join(root, REGISTRY_FILE);
|
|
60
|
+
fs.ensureDirSync(path.dirname(registryPath));
|
|
61
|
+
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a new git worktree with scope isolation
|
|
66
|
+
* @param {string} name - Worktree name
|
|
67
|
+
* @param {Object} options - Creation options
|
|
68
|
+
* @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
|
|
69
|
+
* @param {string} [options.baseBranch] - Base branch to create from
|
|
70
|
+
* @param {string} [options.specId] - Associated spec ID for standard+ modes
|
|
71
|
+
* @returns {Object} Created worktree info
|
|
72
|
+
*/
|
|
73
|
+
function createWorktree(name, options = {}) {
|
|
74
|
+
const root = getRepoRoot();
|
|
75
|
+
const { scope, baseBranch, specId } = options;
|
|
76
|
+
|
|
77
|
+
// Validate name
|
|
78
|
+
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
79
|
+
throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const registry = loadRegistry(root);
|
|
83
|
+
|
|
84
|
+
// Check for duplicate
|
|
85
|
+
if (registry.worktrees[name]) {
|
|
86
|
+
throw new Error(`Worktree '${name}' already exists. Use 'caws worktree destroy ${name}' first.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const worktreePath = path.join(root, WORKTREES_DIR, name);
|
|
90
|
+
const branchName = BRANCH_PREFIX + name;
|
|
91
|
+
const base = baseBranch || getCurrentBranch();
|
|
92
|
+
|
|
93
|
+
// Create the worktree directory
|
|
94
|
+
fs.ensureDirSync(path.dirname(worktreePath));
|
|
95
|
+
|
|
96
|
+
// Create git worktree with new branch
|
|
97
|
+
try {
|
|
98
|
+
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
|
|
99
|
+
cwd: root,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Branch might already exist
|
|
104
|
+
if (error.message.includes('already exists')) {
|
|
105
|
+
execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
|
|
106
|
+
cwd: root,
|
|
107
|
+
stdio: 'pipe',
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
throw new Error(`Failed to create worktree: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Set up sparse checkout if scope is provided
|
|
115
|
+
if (scope) {
|
|
116
|
+
try {
|
|
117
|
+
execFileSync('git', ['sparse-checkout', 'init', '--cone'], {
|
|
118
|
+
cwd: worktreePath,
|
|
119
|
+
stdio: 'pipe',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Parse scope patterns (comma-separated)
|
|
123
|
+
const patterns = scope.split(',').map((p) => p.trim());
|
|
124
|
+
execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
|
|
125
|
+
cwd: worktreePath,
|
|
126
|
+
stdio: 'pipe',
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn(chalk.yellow(`⚠️ Sparse checkout setup failed: ${error.message}`));
|
|
130
|
+
console.warn(chalk.blue('💡 Worktree created but without sparse checkout'));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Copy .caws/ config into worktree
|
|
135
|
+
const cawsSource = path.join(root, '.caws');
|
|
136
|
+
const cawsDest = path.join(worktreePath, '.caws');
|
|
137
|
+
if (fs.existsSync(cawsSource)) {
|
|
138
|
+
try {
|
|
139
|
+
fs.copySync(cawsSource, cawsDest, {
|
|
140
|
+
filter: (src) => {
|
|
141
|
+
// Don't copy worktrees directory or registry into the worktree
|
|
142
|
+
const rel = path.relative(cawsSource, src);
|
|
143
|
+
return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-fatal
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Generate working spec if in standard+ mode and specId provided
|
|
152
|
+
if (specId) {
|
|
153
|
+
try {
|
|
154
|
+
const { generateWorkingSpec } = require('../generators/working-spec');
|
|
155
|
+
const specContent = generateWorkingSpec({
|
|
156
|
+
projectId: specId,
|
|
157
|
+
projectTitle: `Worktree: ${name}`,
|
|
158
|
+
projectDescription: `Isolated worktree for ${name}`,
|
|
159
|
+
riskTier: 3,
|
|
160
|
+
projectMode: 'feature',
|
|
161
|
+
scopeIn: scope || 'src/',
|
|
162
|
+
scopeOut: 'node_modules/, dist/, build/',
|
|
163
|
+
maxFiles: 25,
|
|
164
|
+
maxLoc: 1000,
|
|
165
|
+
blastModules: scope || 'src',
|
|
166
|
+
dataMigration: false,
|
|
167
|
+
rollbackSlo: '5m',
|
|
168
|
+
projectThreats: '',
|
|
169
|
+
projectInvariants: 'System maintains data consistency',
|
|
170
|
+
acceptanceCriteria: 'Given current state, when action occurs, then expected result',
|
|
171
|
+
a11yRequirements: 'keyboard',
|
|
172
|
+
perfBudget: 250,
|
|
173
|
+
securityRequirements: 'validation',
|
|
174
|
+
contractType: '',
|
|
175
|
+
contractPath: '',
|
|
176
|
+
observabilityLogs: '',
|
|
177
|
+
observabilityMetrics: '',
|
|
178
|
+
observabilityTraces: '',
|
|
179
|
+
migrationPlan: '',
|
|
180
|
+
rollbackPlan: '',
|
|
181
|
+
needsOverride: false,
|
|
182
|
+
isExperimental: false,
|
|
183
|
+
aiConfidence: 0.8,
|
|
184
|
+
uncertaintyAreas: '',
|
|
185
|
+
complexityFactors: '',
|
|
186
|
+
});
|
|
187
|
+
const specPath = path.join(cawsDest, 'working-spec.yaml');
|
|
188
|
+
fs.ensureDirSync(path.dirname(specPath));
|
|
189
|
+
fs.writeFileSync(specPath, specContent);
|
|
190
|
+
} catch {
|
|
191
|
+
// Non-fatal: spec generation is optional
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Register worktree
|
|
196
|
+
const entry = {
|
|
197
|
+
name,
|
|
198
|
+
path: worktreePath,
|
|
199
|
+
branch: branchName,
|
|
200
|
+
baseBranch: base,
|
|
201
|
+
scope: scope || null,
|
|
202
|
+
specId: specId || null,
|
|
203
|
+
createdAt: new Date().toISOString(),
|
|
204
|
+
status: 'active',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
registry.worktrees[name] = entry;
|
|
208
|
+
saveRegistry(root, registry);
|
|
209
|
+
|
|
210
|
+
return entry;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* List all registered worktrees with filesystem validation
|
|
215
|
+
* @returns {Array} Worktree entries with status
|
|
216
|
+
*/
|
|
217
|
+
function listWorktrees() {
|
|
218
|
+
const root = getRepoRoot();
|
|
219
|
+
const registry = loadRegistry(root);
|
|
220
|
+
|
|
221
|
+
// Get actual git worktrees for validation
|
|
222
|
+
let gitWorktrees = [];
|
|
223
|
+
try {
|
|
224
|
+
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
225
|
+
cwd: root,
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
});
|
|
228
|
+
gitWorktrees = output
|
|
229
|
+
.split('\n\n')
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.map((block) => {
|
|
232
|
+
const lines = block.split('\n');
|
|
233
|
+
const worktreeLine = lines.find((l) => l.startsWith('worktree '));
|
|
234
|
+
return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
|
|
235
|
+
})
|
|
236
|
+
.filter(Boolean);
|
|
237
|
+
} catch {
|
|
238
|
+
// Git worktree list failed
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const entries = Object.values(registry.worktrees).map((entry) => {
|
|
242
|
+
const exists = fs.existsSync(entry.path);
|
|
243
|
+
const inGit = gitWorktrees.some(
|
|
244
|
+
(wt) => path.resolve(wt) === path.resolve(entry.path)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...entry,
|
|
249
|
+
status: exists && inGit ? 'active' : exists ? 'orphaned' : 'missing',
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Destroy a worktree
|
|
258
|
+
* @param {string} name - Worktree name
|
|
259
|
+
* @param {Object} options - Destruction options
|
|
260
|
+
* @param {boolean} [options.deleteBranch] - Also delete the branch
|
|
261
|
+
* @param {boolean} [options.force] - Force removal even if dirty
|
|
262
|
+
*/
|
|
263
|
+
function destroyWorktree(name, options = {}) {
|
|
264
|
+
const root = getRepoRoot();
|
|
265
|
+
const registry = loadRegistry(root);
|
|
266
|
+
const { deleteBranch = false, force = false } = options;
|
|
267
|
+
|
|
268
|
+
const entry = registry.worktrees[name];
|
|
269
|
+
if (!entry) {
|
|
270
|
+
throw new Error(`Worktree '${name}' not found in registry`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Remove git worktree
|
|
274
|
+
try {
|
|
275
|
+
const args = ['worktree', 'remove'];
|
|
276
|
+
if (force) args.push('--force');
|
|
277
|
+
args.push(entry.path);
|
|
278
|
+
execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (force) {
|
|
281
|
+
// Force cleanup: remove directory manually
|
|
282
|
+
if (fs.existsSync(entry.path)) {
|
|
283
|
+
fs.removeSync(entry.path);
|
|
284
|
+
}
|
|
285
|
+
// Prune git worktree list
|
|
286
|
+
try {
|
|
287
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
288
|
+
} catch {
|
|
289
|
+
// Non-fatal
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Optionally delete branch
|
|
297
|
+
if (deleteBranch && entry.branch) {
|
|
298
|
+
try {
|
|
299
|
+
execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
300
|
+
} catch {
|
|
301
|
+
if (force) {
|
|
302
|
+
try {
|
|
303
|
+
execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
304
|
+
} catch {
|
|
305
|
+
// Non-fatal
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Update registry
|
|
312
|
+
registry.worktrees[name].status = 'destroyed';
|
|
313
|
+
registry.worktrees[name].destroyedAt = new Date().toISOString();
|
|
314
|
+
saveRegistry(root, registry);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Prune stale worktree entries
|
|
319
|
+
* @param {Object} options - Prune options
|
|
320
|
+
* @param {number} [options.maxAgeDays] - Remove entries older than this many days
|
|
321
|
+
* @returns {Array} Pruned entries
|
|
322
|
+
*/
|
|
323
|
+
function pruneWorktrees(options = {}) {
|
|
324
|
+
const root = getRepoRoot();
|
|
325
|
+
const registry = loadRegistry(root);
|
|
326
|
+
const { maxAgeDays = 30 } = options;
|
|
327
|
+
|
|
328
|
+
const now = new Date();
|
|
329
|
+
const pruned = [];
|
|
330
|
+
|
|
331
|
+
for (const [name, entry] of Object.entries(registry.worktrees)) {
|
|
332
|
+
const created = new Date(entry.createdAt);
|
|
333
|
+
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
|
334
|
+
|
|
335
|
+
const shouldPrune =
|
|
336
|
+
entry.status === 'destroyed' ||
|
|
337
|
+
(!fs.existsSync(entry.path) && ageDays > maxAgeDays) ||
|
|
338
|
+
(maxAgeDays === 0 && entry.status === 'destroyed');
|
|
339
|
+
|
|
340
|
+
if (shouldPrune) {
|
|
341
|
+
// Clean up filesystem if still exists
|
|
342
|
+
if (fs.existsSync(entry.path)) {
|
|
343
|
+
try {
|
|
344
|
+
execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
|
|
345
|
+
cwd: root,
|
|
346
|
+
stdio: 'pipe',
|
|
347
|
+
});
|
|
348
|
+
} catch {
|
|
349
|
+
fs.removeSync(entry.path);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
pruned.push(entry);
|
|
353
|
+
delete registry.worktrees[name];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Prune git's worktree list
|
|
358
|
+
try {
|
|
359
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
360
|
+
} catch {
|
|
361
|
+
// Non-fatal
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
saveRegistry(root, registry);
|
|
365
|
+
return pruned;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
createWorktree,
|
|
370
|
+
listWorktrees,
|
|
371
|
+
destroyWorktree,
|
|
372
|
+
pruneWorktrees,
|
|
373
|
+
loadRegistry,
|
|
374
|
+
getRepoRoot,
|
|
375
|
+
WORKTREES_DIR,
|
|
376
|
+
REGISTRY_FILE,
|
|
377
|
+
BRANCH_PREFIX,
|
|
378
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paths.design/caws-cli",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
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": {
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"url": "https://github.com/Paths-Design/coding-agent-working-standard.git",
|
|
52
52
|
"directory": "packages/caws-cli"
|
|
53
53
|
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"registry": "https://registry.npmjs.org/",
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
54
58
|
"dependencies": {
|
|
55
59
|
"chalk": "4.1.2",
|
|
56
60
|
"commander": "^11.0.0",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CAWS Lite Scope Configuration",
|
|
4
|
+
"description": "Scope configuration for CAWS lite mode — guardrails without YAML specs",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version", "allowedDirectories"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": {
|
|
9
|
+
"type": "integer",
|
|
10
|
+
"const": 1,
|
|
11
|
+
"description": "Schema version"
|
|
12
|
+
},
|
|
13
|
+
"allowedDirectories": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": { "type": "string" },
|
|
16
|
+
"minItems": 1,
|
|
17
|
+
"description": "Directories the agent is allowed to modify (e.g., src/, tests/)"
|
|
18
|
+
},
|
|
19
|
+
"bannedPatterns": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"files": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": { "type": "string" },
|
|
25
|
+
"description": "Glob patterns for banned file names (e.g., *-enhanced.*, *-final.*)"
|
|
26
|
+
},
|
|
27
|
+
"directories": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": { "type": "string" },
|
|
30
|
+
"description": "Glob patterns for banned directory names (e.g., *venv*, .venv)"
|
|
31
|
+
},
|
|
32
|
+
"docs": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "string" },
|
|
35
|
+
"description": "Glob patterns for banned doc file names (e.g., *-summary.md)"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
},
|
|
40
|
+
"maxNewFilesPerCommit": {
|
|
41
|
+
"type": "integer",
|
|
42
|
+
"minimum": 1,
|
|
43
|
+
"maximum": 100,
|
|
44
|
+
"description": "Maximum number of new files allowed per commit (prevents file sprawl)"
|
|
45
|
+
},
|
|
46
|
+
"designatedVenvPath": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "The only allowed virtual environment path (e.g., .venv)"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"contracts"
|
|
17
17
|
],
|
|
18
18
|
"properties": {
|
|
19
|
-
"id": { "type": "string", "pattern": "^
|
|
19
|
+
"id": { "type": "string", "pattern": "^[A-Z]{2,6}-\\d{3,4}$" },
|
|
20
20
|
"title": { "type": "string", "minLength": 10, "maxLength": 200 },
|
|
21
21
|
"risk_tier": { "type": ["integer", "string"], "enum": [1, 2, 3, "1", "2", "3"] },
|
|
22
22
|
"mode": { "type": "string", "enum": ["feature", "refactor", "fix", "doc", "chore"] },
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "CAWS Worktree Registry",
|
|
4
|
+
"description": "Registry of git worktrees managed by CAWS for agent scope isolation",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version", "worktrees"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": {
|
|
9
|
+
"type": "integer",
|
|
10
|
+
"const": 1
|
|
11
|
+
},
|
|
12
|
+
"worktrees": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["name", "path", "branch", "baseBranch", "createdAt", "status"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
|
|
19
|
+
"path": { "type": "string" },
|
|
20
|
+
"branch": { "type": "string" },
|
|
21
|
+
"baseBranch": { "type": "string" },
|
|
22
|
+
"scope": { "type": ["string", "null"] },
|
|
23
|
+
"specId": { "type": ["string", "null"] },
|
|
24
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
25
|
+
"destroyedAt": { "type": "string", "format": "date-time" },
|
|
26
|
+
"status": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["active", "orphaned", "missing", "destroyed"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
}
|
|
@@ -65,11 +65,44 @@ DANGEROUS_PATTERNS=(
|
|
|
65
65
|
'reboot'
|
|
66
66
|
'init 0'
|
|
67
67
|
'init 6'
|
|
68
|
+
|
|
69
|
+
# Git destructive operations
|
|
70
|
+
'git init'
|
|
71
|
+
'git reset --hard'
|
|
72
|
+
'git push --force'
|
|
73
|
+
'git push -f '
|
|
74
|
+
'git push --force-with-lease'
|
|
75
|
+
'git clean -f'
|
|
76
|
+
'git checkout \.'
|
|
77
|
+
'git restore \.'
|
|
78
|
+
|
|
79
|
+
# Virtual environment creation (prevents venv sprawl)
|
|
80
|
+
'python -m venv'
|
|
81
|
+
'python3 -m venv'
|
|
82
|
+
'virtualenv '
|
|
83
|
+
'conda create'
|
|
68
84
|
)
|
|
69
85
|
|
|
70
86
|
# Check command against dangerous patterns
|
|
71
87
|
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
72
88
|
if echo "$COMMAND" | grep -qiE "$pattern"; then
|
|
89
|
+
# Allow git init in worktree context
|
|
90
|
+
if [[ "$pattern" == "git init" ]] && [[ "${CAWS_WORKTREE_CONTEXT:-0}" == "1" ]]; then
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Allow venv commands if target matches designated venv path from scope.json
|
|
95
|
+
if echo "$pattern" | grep -qE '(python.*venv|virtualenv|conda create)'; then
|
|
96
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
97
|
+
SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
|
|
98
|
+
if [[ -f "$SCOPE_FILE" ]] && command -v node >/dev/null 2>&1; then
|
|
99
|
+
DESIGNATED_VENV=$(node -e "try { const s = JSON.parse(require('fs').readFileSync('$SCOPE_FILE','utf8')); console.log(s.designatedVenvPath || ''); } catch(e) { console.log(''); }" 2>/dev/null || echo "")
|
|
100
|
+
if [[ -n "$DESIGNATED_VENV" ]] && echo "$COMMAND" | grep -qF "$DESIGNATED_VENV"; then
|
|
101
|
+
continue
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
73
106
|
# Output to stderr for Claude to see
|
|
74
107
|
echo "BLOCKED: Command matches dangerous pattern: $pattern" >&2
|
|
75
108
|
echo "Command was: $COMMAND" >&2
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Lite-Mode Sprawl Check Hook
|
|
3
|
+
# Checks for file sprawl patterns (banned names, venv dirs, doc sprawl)
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Read JSON input from Claude Code
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
# Extract tool info
|
|
12
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
13
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
14
|
+
|
|
15
|
+
# Only check Write operations (new file creation)
|
|
16
|
+
if [[ "$TOOL_NAME" != "Write" ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
25
|
+
SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
|
|
26
|
+
|
|
27
|
+
# Only active in lite mode (scope.json present, no working-spec.yaml)
|
|
28
|
+
if [[ ! -f "$SCOPE_FILE" ]]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Get relative path
|
|
33
|
+
# Get relative path (portable — macOS realpath lacks --relative-to)
|
|
34
|
+
if [[ "$FILE_PATH" == "$PROJECT_DIR"/* ]]; then
|
|
35
|
+
REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
|
|
36
|
+
else
|
|
37
|
+
REL_PATH="$FILE_PATH"
|
|
38
|
+
fi
|
|
39
|
+
BASENAME=$(basename "$REL_PATH")
|
|
40
|
+
|
|
41
|
+
# Use Node.js to check banned patterns
|
|
42
|
+
if command -v node >/dev/null 2>&1; then
|
|
43
|
+
SPRAWL_CHECK=$(node -e "
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
try {
|
|
47
|
+
const scope = JSON.parse(fs.readFileSync('$SCOPE_FILE', 'utf8'));
|
|
48
|
+
const filePath = '$REL_PATH';
|
|
49
|
+
const basename = '$BASENAME';
|
|
50
|
+
const banned = scope.bannedPatterns || {};
|
|
51
|
+
|
|
52
|
+
function matchGlob(str, pattern) {
|
|
53
|
+
const regex = new RegExp('^' + pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.') + '$');
|
|
54
|
+
return regex.test(str);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check banned file patterns
|
|
58
|
+
for (const p of (banned.files || [])) {
|
|
59
|
+
if (matchGlob(basename, p)) {
|
|
60
|
+
console.log('banned_file:' + p);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check banned doc patterns
|
|
66
|
+
for (const p of (banned.docs || [])) {
|
|
67
|
+
if (matchGlob(basename, p)) {
|
|
68
|
+
console.log('banned_doc:' + p);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check banned directory patterns
|
|
74
|
+
const parts = filePath.split('/');
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
for (const p of (banned.directories || [])) {
|
|
77
|
+
if (matchGlob(part, p)) {
|
|
78
|
+
console.log('banned_dir:' + p + ':' + part);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('ok');
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.log('error:' + error.message);
|
|
87
|
+
}
|
|
88
|
+
" 2>&1)
|
|
89
|
+
|
|
90
|
+
if [[ "$SPRAWL_CHECK" == banned_file:* ]]; then
|
|
91
|
+
PATTERN="${SPRAWL_CHECK#banned_file:}"
|
|
92
|
+
echo "BLOCKED: File name matches banned sprawl pattern: $PATTERN" >&2
|
|
93
|
+
echo "File: $REL_PATH" >&2
|
|
94
|
+
echo "Banned patterns prevent shadow files like *-enhanced.*, *-final.*, *-v2.*, *-copy.*" >&2
|
|
95
|
+
echo "Instead, modify the original file directly." >&2
|
|
96
|
+
exit 2
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [[ "$SPRAWL_CHECK" == banned_doc:* ]]; then
|
|
100
|
+
PATTERN="${SPRAWL_CHECK#banned_doc:}"
|
|
101
|
+
echo "BLOCKED: Doc file matches banned sprawl pattern: $PATTERN" >&2
|
|
102
|
+
echo "File: $REL_PATH" >&2
|
|
103
|
+
echo "Avoid creating many summary/recap/plan files. Update existing documentation instead." >&2
|
|
104
|
+
exit 2
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [[ "$SPRAWL_CHECK" == banned_dir:* ]]; then
|
|
108
|
+
IFS=':' read -r _ PATTERN DIR_NAME <<< "$SPRAWL_CHECK"
|
|
109
|
+
echo "BLOCKED: Directory matches banned pattern: $PATTERN (directory: $DIR_NAME)" >&2
|
|
110
|
+
echo "File: $REL_PATH" >&2
|
|
111
|
+
echo "Use the designated venv path instead of creating new virtual environments." >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Allow the operation
|
|
117
|
+
exit 0
|