@paths.design/caws-cli 9.2.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/commands/specs.js +28 -15
  2. package/dist/commands/status.js +1 -1
  3. package/dist/commands/verify-acs.js +471 -0
  4. package/dist/index.js +13 -1
  5. package/dist/parallel/parallel-manager.js +5 -12
  6. package/dist/scaffold/cursor-hooks.js +0 -1
  7. package/dist/scaffold/git-hooks.js +18 -1
  8. package/dist/templates/.caws/tools/README.md +4 -7
  9. package/dist/templates/.caws/tools/scope-guard.js +115 -171
  10. package/dist/templates/.claude/hooks/audit.sh +25 -0
  11. package/dist/templates/.claude/hooks/block-dangerous.sh +39 -0
  12. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
  13. package/dist/templates/.claude/hooks/naming-check.sh +5 -2
  14. package/dist/templates/.claude/hooks/scope-guard.sh +66 -4
  15. package/dist/templates/.claude/hooks/session-log.sh +38 -5
  16. package/dist/templates/.claude/rules/worktree-isolation.md +4 -1
  17. package/dist/templates/.cursor/README.md +0 -9
  18. package/dist/templates/.cursor/hooks/audit.sh +1 -1
  19. package/dist/templates/.cursor/hooks/block-dangerous.sh +1 -0
  20. package/dist/templates/.cursor/hooks/scan-secrets.sh +8 -3
  21. package/dist/templates/.cursor/hooks.json +0 -8
  22. package/dist/templates/.vscode/launch.json +0 -12
  23. package/dist/utils/detection.js +37 -0
  24. package/dist/utils/project-analysis.js +0 -1
  25. package/dist/utils/spec-resolver.js +23 -10
  26. package/dist/worktree/worktree-manager.js +18 -1
  27. package/package.json +1 -1
  28. package/templates/.caws/tools/README.md +4 -7
  29. package/templates/.caws/tools/scope-guard.js +115 -171
  30. package/templates/.claude/hooks/audit.sh +25 -0
  31. package/templates/.claude/hooks/block-dangerous.sh +39 -0
  32. package/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
  33. package/templates/.claude/hooks/naming-check.sh +5 -2
  34. package/templates/.claude/hooks/scope-guard.sh +66 -4
  35. package/templates/.claude/hooks/session-log.sh +38 -5
  36. package/templates/.claude/rules/worktree-isolation.md +4 -1
  37. package/templates/.cursor/README.md +0 -9
  38. package/templates/.cursor/hooks/audit.sh +1 -1
  39. package/templates/.cursor/hooks/block-dangerous.sh +1 -0
  40. package/templates/.cursor/hooks/scan-secrets.sh +8 -3
  41. package/templates/.cursor/hooks.json +0 -8
  42. package/templates/.vscode/launch.json +0 -12
  43. package/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
@@ -157,6 +157,33 @@ def rel(path):
157
157
  return path[len(cwd) + 1:]
158
158
  return path or ""
159
159
 
160
+ def decode_structured_text_payload(raw):
161
+ """Decode JSON-escaped text payloads (e.g., Agent/Task tool outputs)."""
162
+ if not isinstance(raw, str):
163
+ return raw
164
+ payload = raw.strip()
165
+ if not payload or payload[0] not in "[{":
166
+ return raw
167
+ try:
168
+ parsed = json.loads(payload)
169
+ except Exception:
170
+ return raw
171
+
172
+ text_blocks = []
173
+ if isinstance(parsed, dict):
174
+ parsed = [parsed]
175
+ if isinstance(parsed, list):
176
+ for item in parsed:
177
+ if not isinstance(item, dict):
178
+ continue
179
+ text = item.get("text")
180
+ if isinstance(text, str) and text.strip():
181
+ text_blocks.append(text)
182
+
183
+ if text_blocks:
184
+ return "\n\n".join(text_blocks)
185
+ return raw
186
+
160
187
  # ---- Accumulate turns as chronological event timelines ----
161
188
  turns = []
162
189
  # Each turn: {user, timeline: [{kind, ...}, ...], edits, reads, searches, commands}
@@ -242,18 +269,24 @@ for line in sys.stdin:
242
269
  tool_info = pending_tools.get(tid, {})
243
270
  name = tool_info.get("name", "unknown")
244
271
 
245
- # Decide if this result is notable enough to show inline
246
- # Task results are always notable (subagent did substantive work)
247
- notable = is_error or name == "Task"
272
+ # Always capture tool results for Bash, Task, Agent.
273
+ # For Read/Write/Edit, only capture if notable (errors, test output, etc.)
274
+ # to avoid dumping entire file contents into turn logs.
275
+ always_capture = name in ("Bash", "Task", "Agent")
276
+ notable = is_error
248
277
  if not notable and content:
249
278
  content_lower = content.lower()
250
279
  notable = any(kw.lower() in content_lower for kw in NOTABLE_KW)
251
280
 
252
- if notable and content:
281
+ if (always_capture or notable) and content:
253
282
  # Cap file-content tools (full file reads/writes blow out turn files)
254
283
  display = content
255
284
  if name in ("Read", "Write", "Edit") and len(content) > 2000:
256
285
  display = content[:2000] + "\n...(file content truncated)"
286
+ elif name in ("Task", "Agent"):
287
+ display = decode_structured_text_payload(content)
288
+ elif name == "Bash" and len(content) > 5000:
289
+ display = content[:5000] + "\n...(output truncated at 5000 chars)"
257
290
  # Graft result onto the original tool_call entry (not a separate timeline item)
258
291
  if tool_info:
259
292
  tool_info["output"] = display
@@ -281,7 +314,7 @@ for i, turn in enumerate(turns):
281
314
  md_lines = [f"# Turn {num}", ""]
282
315
 
283
316
  if turn["user"]:
284
- md_lines.extend([f"> ---user---\n{turn['user']}\n---/user---", ""])
317
+ md_lines.extend([f"> ---user---\n{turn['user']}\n---\/user---", ""])
285
318
 
286
319
  for event in turn["timeline"]:
287
320
  kind = event["kind"]
@@ -12,16 +12,19 @@ When multiple agents are working on this project, each agent MUST work in its ow
12
12
  1. Check if worktrees exist: `caws worktree list` shows all active worktrees with last commit time and owner
13
13
  2. If worktrees are active and you are on the base branch, switch to your assigned worktree
14
14
  3. If no worktree exists for you, create one with `caws worktree create <name>` or `caws parallel setup <plan-file>`
15
+ 4. **Never touch a worktree you did not create.** Do not destroy, prune, stash, or "clean up" another agent's worktree — even if it looks stale. Another agent may be actively working in it. If you think a worktree is abandoned, leave it alone and let the user decide.
15
16
 
16
17
  ## Forbidden operations when worktrees are active
17
18
 
18
19
  - `git commit --amend` -- rewrites history that other agents depend on
20
+ - `git rebase` -- rewrites branch history; the hook blocks this automatically while worktrees are active. If you need code from main, create a new worktree from current main instead
21
+ - `git cherry-pick` -- replays commits across branches; blocked while worktrees are active to prevent cross-boundary contamination
19
22
  - `git stash` / `git stash pop` -- stash is shared across all worktrees; using it can destroy another agent's uncommitted work
20
23
  - `git reset --hard` -- discards work that other agents may depend on
21
24
  - `git push --force` -- rewrites remote history
22
25
  - Direct commits to the base branch -- only `merge(worktree):` and `wip(checkpoint):` formats are allowed
23
26
  - Copying files between your worktree and the main repo directory -- defeats isolation
24
- - Destroying another agent's active worktree -- `caws worktree destroy` will block this unless you use `--force`
27
+ - Destroying another agent's worktree -- even with `--force`. If you did not create it, do not destroy it. Period.
25
28
 
26
29
  ## Merging worktree branches back to base
27
30
 
@@ -9,7 +9,6 @@ Cursor hooks enable seamless integration between CAWS and the Cursor IDE, provid
9
9
  - **Real-time quality validation** as you code
10
10
  - **Automatic spec validation** when editing working specs
11
11
  - **Scope enforcement** preventing out-of-scope file access
12
- - **Tool validation** for safe MCP execution
13
12
  - **Quality monitoring** after file edits
14
13
 
15
14
  ## Hook Configuration
@@ -19,7 +18,6 @@ The `hooks.json` file defines when each hook runs:
19
18
  ```json
20
19
  {
21
20
  "beforeShellExecution": ["block-dangerous.sh", "audit.sh"],
22
- "beforeMCPExecution": ["audit.sh", "caws-tool-validation.sh"],
23
21
  "beforeReadFile": ["scan-secrets.sh", "caws-scope-guard.sh"],
24
22
  "afterFileEdit": ["format.sh", "naming-check.sh", "validate-spec.sh", "caws-quality-check.sh", "audit.sh"],
25
23
  "beforeSubmitPrompt": ["caws-scope-guard.sh", "audit.sh"],
@@ -43,12 +41,6 @@ The `hooks.json` file defines when each hook runs:
43
41
  - **Blocks**: Yes (for out-of-scope file access)
44
42
  - **Requires**: `.caws/working-spec.yaml`
45
43
 
46
- #### `caws-tool-validation.sh`
47
- - **Trigger**: `beforeMCPExecution`
48
- - **Purpose**: Validates CAWS MCP tool calls for security
49
- - **Blocks**: Yes (for dangerous operations or invalid waivers)
50
- - **Validates**: Waiver creation, tool permissions, command safety
51
-
52
44
  ### General Security Hooks
53
45
 
54
46
  #### `block-dangerous.sh`
@@ -242,7 +234,6 @@ Cursor Hooks ←→ CAWS CLI ←→ VS Code Extension
242
234
 
243
235
  - **Git Hooks**: `.git/hooks/` for commit/push validation
244
236
  - **VS Code Extension**: Rich UI for CAWS operations
245
- - **MCP Server**: Agent tool integration
246
237
  - **CAWS CLI**: Core functionality
247
238
 
248
239
  ### Data Flow
@@ -2,7 +2,7 @@
2
2
  # Cursor Hook: Audit Trail
3
3
  #
4
4
  # Purpose: Log all Cursor AI events for provenance tracking
5
- # Event: All (beforeShellExecution, beforeMCPExecution, beforeReadFile,
5
+ # Event: All (beforeShellExecution, beforeReadFile,
6
6
  # afterFileEdit, beforeSubmitPrompt, stop)
7
7
  #
8
8
  # @author @darianrosebrook
@@ -29,6 +29,7 @@ HARD_BLOCKS=(
29
29
  "git commit --amend --no-edit" # Can rewrite commit history destructively
30
30
  "git reset --hard" # Can lose uncommitted work and stashed changes
31
31
  "git push --force" # Can overwrite remote repository history
32
+ "git rebase" # Rewrites branch history; blocked while worktrees are active
32
33
  "dd if="
33
34
  "mkfs"
34
35
  "format c:"
@@ -28,14 +28,19 @@ if [[ "$FILE_PATH" =~ \.(pem|key|p12|pfx|cert|crt)$ ]]; then
28
28
  fi
29
29
 
30
30
  # Scan content for common secret patterns
31
- if echo "$CONTENT" | grep -qiE "(api[_-]?key|secret[_-]?key|password|private[_-]?key|access[_-]?token|bearer\s+[A-Za-z0-9_\-\.]+|AKIA[0-9A-Z]{16})"; then
31
+ # bearer requires 20+ chars to avoid false positives on short tokens in docs
32
+ # AKIA prefix is specific to AWS access keys
33
+ if echo "$CONTENT" | grep -qiE "(api[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|secret[_-]?key\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|password\s*[:=]\s*['\"]?[^\s'\"]{8,}|private[_-]?key\s*[:=]|access[_-]?token\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}|[Bb]earer\s+[A-Za-z0-9_\-\.]{20,}|AKIA[0-9A-Z]{16})"; then
32
34
  # Don't block, but warn
33
35
  echo '{"permission":"allow","userMessage":"⚠️ Warning: Potential secrets detected in file. Ensure they are not committed.","agentMessage":"This file may contain secrets. Use placeholder values or environment variables."}' 2>/dev/null
34
36
  exit 0
35
37
  fi
36
38
 
37
- # Check for common PII patterns (SSN, credit card, etc.)
38
- if echo "$CONTENT" | grep -qE "([0-9]{3}-[0-9]{2}-[0-9]{4}|[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4})"; then
39
+ # Check for common PII patterns (SSN, credit card)
40
+ # SSN: exactly 3-2-4 digit pattern (not inside longer numbers)
41
+ # Credit card: require at least a Luhn-plausible 13-19 digit sequence with separators
42
+ if echo "$CONTENT" | grep -qE "(^|[^0-9])[0-9]{3}-[0-9]{2}-[0-9]{4}($|[^0-9])" || \
43
+ echo "$CONTENT" | grep -qE "(^|[^0-9])[0-9]{4}[- ][0-9]{4}[- ][0-9]{4}[- ][0-9]{4}($|[^0-9])"; then
39
44
  echo '{"permission":"allow","userMessage":"⚠️ Warning: Potential PII detected. Ensure compliance with data protection policies.","agentMessage":"This file may contain PII (SSN, credit card). Use anonymized test data."}' 2>/dev/null
40
45
  exit 0
41
46
  fi
@@ -9,14 +9,6 @@
9
9
  "command": "./.cursor/hooks/audit.sh"
10
10
  }
11
11
  ],
12
- "beforeMCPExecution": [
13
- {
14
- "command": "./.cursor/hooks/audit.sh"
15
- },
16
- {
17
- "command": "./.cursor/hooks/caws-tool-validation.sh"
18
- }
19
- ],
20
12
  "beforeReadFile": [
21
13
  {
22
14
  "command": "./.cursor/hooks/scan-secrets.sh"
@@ -1,18 +1,6 @@
1
1
  {
2
2
  "version": "0.2.0",
3
3
  "configurations": [
4
- {
5
- "name": "Debug MCP Server",
6
- "type": "node",
7
- "request": "launch",
8
- "program": "${workspaceFolder}/packages/caws-mcp-server/index.js",
9
- "args": [],
10
- "env": {
11
- "NODE_ENV": "development",
12
- "CAWS_DEBUG": "true"
13
- },
14
- "console": "integratedTerminal"
15
- },
16
4
  {
17
5
  "name": "Debug CAWS CLI",
18
6
  "type": "node",
@@ -196,7 +196,44 @@ function detectCAWSSetup(cwd = process.cwd()) {
196
196
  };
197
197
  }
198
198
 
199
+ /**
200
+ * Find the CAWS project root by walking up from startDir looking for .caws/
201
+ * Falls back to git root, then to process.cwd()
202
+ * @param {string} [startDir] - Directory to start searching from
203
+ * @returns {string} Project root directory path
204
+ */
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
217
+ try {
218
+ const { execFileSync } = require('child_process');
219
+ const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
220
+ encoding: 'utf8',
221
+ cwd: startDir,
222
+ stdio: ['pipe', 'pipe', 'pipe'],
223
+ }).trim();
224
+ if (gitRoot && fs.existsSync(path.join(gitRoot, '.caws'))) {
225
+ return gitRoot;
226
+ }
227
+ } catch {
228
+ // Not a git repo or git not available
229
+ }
230
+
231
+ // Final fallback: cwd
232
+ return process.cwd();
233
+ }
234
+
199
235
  module.exports = {
200
236
  detectCAWSSetup,
201
237
  findPackageRoot,
238
+ findProjectRoot,
202
239
  };
@@ -347,7 +347,6 @@ function getTodoAnalyzerSuggestion(cwd = process.cwd()) {
347
347
  suggestions.push(
348
348
  ' - Install Node.js: https://nodejs.org/ (then use: npx --yes @paths.design/quality-gates)'
349
349
  );
350
- suggestions.push(' - Use CAWS MCP server: caws quality-gates (via MCP)');
351
350
  }
352
351
 
353
352
  // Check for project-specific scripts (language-agnostic - if they exist, suggest them)
@@ -12,6 +12,7 @@ const chalk = require('chalk');
12
12
 
13
13
  // Import SPEC_TYPES from constants for consistent display
14
14
  const { SPEC_TYPES } = require('../constants/spec-types');
15
+ const { findProjectRoot } = require('./detection');
15
16
 
16
17
  /**
17
18
  * Spec resolution priority:
@@ -22,6 +23,18 @@ const SPECS_DIR = '.caws/specs';
22
23
  const LEGACY_SPEC = '.caws/working-spec.yaml';
23
24
  const SPECS_REGISTRY = '.caws/specs/registry.json';
24
25
 
26
+ /**
27
+ * Get the project root for spec resolution.
28
+ * Caches per process to avoid repeated filesystem walks.
29
+ */
30
+ let _cachedProjectRoot = null;
31
+ function getProjectRoot() {
32
+ if (!_cachedProjectRoot) {
33
+ _cachedProjectRoot = findProjectRoot();
34
+ }
35
+ return _cachedProjectRoot;
36
+ }
37
+
25
38
  /**
26
39
  * Resolve spec file path based on priority
27
40
  * @param {Object} options - Resolution options
@@ -36,7 +49,7 @@ async function resolveSpec(options = {}) {
36
49
 
37
50
  // 1. Explicit file path takes highest priority
38
51
  if (specFile) {
39
- const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(process.cwd(), specFile);
52
+ const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(getProjectRoot(), specFile);
40
53
 
41
54
  if (await fs.pathExists(explicitPath)) {
42
55
  const yaml = require('js-yaml');
@@ -55,7 +68,7 @@ async function resolveSpec(options = {}) {
55
68
 
56
69
  // 2. Feature-specific spec (preferred for multi-agent)
57
70
  if (specId) {
58
- const featurePath = path.join(process.cwd(), SPECS_DIR, `${specId}.yaml`);
71
+ const featurePath = path.join(getProjectRoot(), SPECS_DIR, `${specId}.yaml`);
59
72
 
60
73
  if (await fs.pathExists(featurePath)) {
61
74
  const yaml = require('js-yaml');
@@ -83,7 +96,7 @@ async function resolveSpec(options = {}) {
83
96
  if (specIds.length === 1) {
84
97
  // Single spec - use it automatically
85
98
  const singleSpecId = specIds[0];
86
- const singleSpecPath = path.join(process.cwd(), SPECS_DIR, registry.specs[singleSpecId].path);
99
+ const singleSpecPath = path.join(getProjectRoot(), SPECS_DIR, registry.specs[singleSpecId].path);
87
100
 
88
101
  if (await fs.pathExists(singleSpecPath)) {
89
102
  const yaml = require('js-yaml');
@@ -179,7 +192,7 @@ async function resolveSpec(options = {}) {
179
192
  }
180
193
 
181
194
  // 4. Fall back to legacy working-spec.yaml (with warning)
182
- const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
195
+ const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
183
196
 
184
197
  if (await fs.pathExists(legacyPath)) {
185
198
  const yaml = require('js-yaml');
@@ -211,7 +224,7 @@ async function resolveSpec(options = {}) {
211
224
  * @returns {Promise<Object>} Registry data
212
225
  */
213
226
  async function loadSpecsRegistry() {
214
- const registryPath = path.join(process.cwd(), SPECS_REGISTRY);
227
+ const registryPath = path.join(getProjectRoot(), SPECS_REGISTRY);
215
228
 
216
229
  if (!(await fs.pathExists(registryPath))) {
217
230
  return {
@@ -241,7 +254,7 @@ async function listAvailableSpecs() {
241
254
  const specs = [];
242
255
 
243
256
  // Check feature-specific specs
244
- const specsDir = path.join(process.cwd(), SPECS_DIR);
257
+ const specsDir = path.join(getProjectRoot(), SPECS_DIR);
245
258
  if (await fs.pathExists(specsDir)) {
246
259
  const files = await fs.readdir(specsDir);
247
260
  const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
@@ -257,7 +270,7 @@ async function listAvailableSpecs() {
257
270
 
258
271
  specs.push({
259
272
  id: spec.id || path.basename(file, path.extname(file)),
260
- path: path.relative(process.cwd(), specPath),
273
+ path: path.relative(getProjectRoot(), specPath),
261
274
  type: 'feature',
262
275
  title: spec.title || 'Untitled',
263
276
  });
@@ -268,7 +281,7 @@ async function listAvailableSpecs() {
268
281
  }
269
282
 
270
283
  // Check legacy working-spec.yaml
271
- const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
284
+ const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
272
285
  if (await fs.pathExists(legacyPath)) {
273
286
  try {
274
287
  const yaml = require('js-yaml');
@@ -342,7 +355,7 @@ async function interactiveSpecSelection(specIds) {
342
355
  async function checkMultiSpecStatus() {
343
356
  const registry = await loadSpecsRegistry();
344
357
  const hasFeatureSpecs = Object.keys(registry.specs ?? {}).length > 0;
345
- const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
358
+ const legacyPath = path.join(getProjectRoot(), LEGACY_SPEC);
346
359
  const hasLegacySpec = await fs.pathExists(legacyPath);
347
360
 
348
361
  return {
@@ -374,7 +387,7 @@ async function checkScopeConflicts(specIds) {
374
387
  try {
375
388
  spec = yaml.load(content);
376
389
  } catch (yamlError) {
377
- const relativePath = path.relative(process.cwd(), specPath);
390
+ const relativePath = path.relative(getProjectRoot(), specPath);
378
391
  throw new Error(
379
392
  `Invalid YAML syntax in ${relativePath}: ${yamlError.message}\n` +
380
393
  (yamlError.mark
@@ -343,10 +343,27 @@ function destroyWorktree(name, options = {}) {
343
343
  `Worktree '${name}' belongs to another session${recency}.\n` +
344
344
  ` Owner: ${entry.owner}\n` +
345
345
  ` You: ${currentSession}\n` +
346
- `Another agent may be actively working here. Use --force to override.`
346
+ `Another agent may be actively working here.\n` +
347
+ `Do NOT destroy worktrees you did not create. Ask the user if cleanup is needed.`
347
348
  );
348
349
  }
349
350
 
351
+ // Even with --force, warn loudly when destroying another session's worktree
352
+ if (
353
+ force &&
354
+ entry.status === 'active' &&
355
+ entry.owner &&
356
+ currentSession &&
357
+ entry.owner !== currentSession
358
+ ) {
359
+ const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
360
+ const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
361
+ console.log(chalk.red(`\n ⚠ WARNING: Force-destroying worktree '${name}' owned by another session${recency}`));
362
+ console.log(chalk.red(` Owner: ${entry.owner}`));
363
+ console.log(chalk.red(` You: ${currentSession}`));
364
+ console.log(chalk.red(` If the other agent is still running, this WILL break their work.\n`));
365
+ }
366
+
350
367
  // Auto-force when the branch is already merged to its base branch.
351
368
  // Dirty files in a merged worktree are definitionally stale.
352
369
  const merged = entry.branch && entry.baseBranch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "9.2.0",
3
+ "version": "9.3.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": {
@@ -4,18 +4,15 @@ This directory contains CAWS-specific tools that aren't available in the CLI.
4
4
 
5
5
  ## scope-guard.js
6
6
 
7
- Enforces that experimental code stays within designated sandbox areas. Used by Cursor hooks for scope validation.
7
+ Checks whether a file is within scope of active working-spec and feature specs. Used by Cursor hooks for scope validation on file attachments.
8
8
 
9
9
  ```bash
10
- # Validate experimental code containment
11
- node .caws/tools/scope-guard.js validate
10
+ # Check if a file is in scope
11
+ node .caws/tools/scope-guard.js check src/index.js
12
12
 
13
- # Check containment status
14
- node .caws/tools/scope-guard.js check .caws/working-spec.yaml
13
+ # Exit code 0 = in scope, 1 = out of scope
15
14
  ```
16
15
 
17
16
  **Usage in Cursor Hooks:**
18
17
 
19
18
  The `.cursor/hooks/scope-guard.sh` hook automatically uses this tool to validate file attachments against working spec scope boundaries.
20
-
21
-