@paths.design/caws-cli 9.1.1 → 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 (48) hide show
  1. package/dist/budget-derivation.js +15 -3
  2. package/dist/commands/specs.js +28 -15
  3. package/dist/commands/status.js +1 -1
  4. package/dist/commands/verify-acs.js +471 -0
  5. package/dist/commands/worktree.js +107 -15
  6. package/dist/index.js +21 -1
  7. package/dist/parallel/parallel-manager.js +5 -12
  8. package/dist/scaffold/cursor-hooks.js +0 -1
  9. package/dist/scaffold/git-hooks.js +18 -1
  10. package/dist/templates/.caws/tools/README.md +4 -7
  11. package/dist/templates/.caws/tools/scope-guard.js +115 -171
  12. package/dist/templates/.claude/hooks/audit.sh +25 -0
  13. package/dist/templates/.claude/hooks/block-dangerous.sh +39 -0
  14. package/dist/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
  15. package/dist/templates/.claude/hooks/naming-check.sh +5 -2
  16. package/dist/templates/.claude/hooks/scope-guard.sh +66 -4
  17. package/dist/templates/.claude/hooks/session-log.sh +38 -5
  18. package/dist/templates/.claude/hooks/worktree-write-guard.sh +13 -1
  19. package/dist/templates/.claude/rules/worktree-isolation.md +36 -4
  20. package/dist/templates/.cursor/README.md +0 -9
  21. package/dist/templates/.cursor/hooks/audit.sh +1 -1
  22. package/dist/templates/.cursor/hooks/block-dangerous.sh +1 -0
  23. package/dist/templates/.cursor/hooks/scan-secrets.sh +8 -3
  24. package/dist/templates/.cursor/hooks.json +0 -8
  25. package/dist/templates/.vscode/launch.json +0 -12
  26. package/dist/utils/detection.js +37 -0
  27. package/dist/utils/project-analysis.js +0 -1
  28. package/dist/utils/spec-resolver.js +23 -10
  29. package/dist/validation/spec-validation.js +8 -0
  30. package/dist/worktree/worktree-manager.js +242 -6
  31. package/package.json +1 -1
  32. package/templates/.caws/tools/README.md +4 -7
  33. package/templates/.caws/tools/scope-guard.js +115 -171
  34. package/templates/.claude/hooks/audit.sh +25 -0
  35. package/templates/.claude/hooks/block-dangerous.sh +39 -0
  36. package/templates/.claude/hooks/lite-sprawl-check.sh +30 -2
  37. package/templates/.claude/hooks/naming-check.sh +5 -2
  38. package/templates/.claude/hooks/scope-guard.sh +66 -4
  39. package/templates/.claude/hooks/session-log.sh +38 -5
  40. package/templates/.claude/hooks/worktree-write-guard.sh +13 -1
  41. package/templates/.claude/rules/worktree-isolation.md +36 -4
  42. package/templates/.cursor/README.md +0 -9
  43. package/templates/.cursor/hooks/audit.sh +1 -1
  44. package/templates/.cursor/hooks/block-dangerous.sh +1 -0
  45. package/templates/.cursor/hooks/scan-secrets.sh +8 -3
  46. package/templates/.cursor/hooks.json +0 -8
  47. package/templates/.vscode/launch.json +0 -12
  48. package/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
@@ -88,6 +88,31 @@ case "$EVENT_TYPE" in
88
88
  ;;
89
89
  esac
90
90
 
91
+ # --- Log rotation ---
92
+ # Keep main audit.log under 10MB; keep date-logs for 30 days
93
+ rotate_logs() {
94
+ # Rotate main audit.log at 10MB
95
+ if [[ -f "$LOG_FILE" ]]; then
96
+ local size
97
+ size=$(wc -c < "$LOG_FILE" 2>/dev/null | tr -d ' ')
98
+ if [[ "$size" -gt 10485760 ]]; then
99
+ # Keep last rotated copy, discard older
100
+ [[ -f "${LOG_FILE}.1" ]] && rm -f "${LOG_FILE}.1"
101
+ mv "$LOG_FILE" "${LOG_FILE}.1"
102
+ fi
103
+ fi
104
+
105
+ # Prune date-based logs older than 30 days
106
+ if [[ -d "$LOG_DIR" ]]; then
107
+ find "$LOG_DIR" -name 'audit-*.log' -type f -mtime +30 -delete 2>/dev/null || true
108
+ fi
109
+ }
110
+
111
+ # Run rotation check ~1% of the time (avoid stat overhead on every tool call)
112
+ if [[ $(( RANDOM % 100 )) -eq 0 ]]; then
113
+ rotate_logs
114
+ fi
115
+
91
116
  # Append to log files
92
117
  echo "$LOG_ENTRY" >> "$LOG_FILE"
93
118
  echo "$LOG_ENTRY" >> "$DATE_LOG_FILE"
@@ -75,6 +75,8 @@ DANGEROUS_PATTERNS=(
75
75
  'git clean -f'
76
76
  'git checkout \.'
77
77
  'git restore \.'
78
+ '(^|&&|\|\||;|\|)\s*git rebase'
79
+ '(^|&&|\|\||;|\|)\s*git cherry-pick'
78
80
 
79
81
  # Virtual environment creation (prevents venv sprawl)
80
82
  'python -m venv'
@@ -91,6 +93,43 @@ for pattern in "${DANGEROUS_PATTERNS[@]}"; do
91
93
  continue
92
94
  fi
93
95
 
96
+ # Allow git rebase/cherry-pick only when no worktrees are active
97
+ if [[ "$pattern" == *"git rebase"* ]] || [[ "$pattern" == *"git cherry-pick"* ]]; then
98
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
99
+ # Resolve to main repo root if we're in a worktree
100
+ if command -v git >/dev/null 2>&1; then
101
+ GIT_COMMON=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
102
+ if [[ -n "$GIT_COMMON" ]] && [[ "$GIT_COMMON" != ".git" ]]; then
103
+ CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON/.." 2>/dev/null && pwd || echo "")
104
+ if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
105
+ PROJECT_DIR="$CANDIDATE"
106
+ fi
107
+ fi
108
+ fi
109
+ WT_FILE="$PROJECT_DIR/.caws/worktrees.json"
110
+ if [[ -f "$WT_FILE" ]] && command -v node >/dev/null 2>&1; then
111
+ ACTIVE_COUNT=$(node -e "
112
+ try {
113
+ var r = JSON.parse(require('fs').readFileSync('$WT_FILE','utf8'));
114
+ var c = Object.values(r.worktrees||{}).filter(function(w){return w.status==='active';}).length;
115
+ console.log(c);
116
+ } catch(e) { console.log(0); }
117
+ " 2>/dev/null || echo "0")
118
+ if [[ "$ACTIVE_COUNT" -gt 0 ]]; then
119
+ # Extract the specific git subcommand for the message
120
+ GIT_SUBCMD="git operation"
121
+ [[ "$pattern" == *"git rebase"* ]] && GIT_SUBCMD="git rebase"
122
+ [[ "$pattern" == *"git cherry-pick"* ]] && GIT_SUBCMD="git cherry-pick"
123
+ echo "BLOCKED: $GIT_SUBCMD is forbidden while $ACTIVE_COUNT worktree(s) are active." >&2
124
+ echo "This can replay or rewrite commits across worktree boundaries." >&2
125
+ echo "Command was: $COMMAND" >&2
126
+ exit 2
127
+ fi
128
+ fi
129
+ # No active worktrees — allow
130
+ continue
131
+ fi
132
+
94
133
  # Allow venv commands if target matches designated venv path from scope.json
95
134
  if echo "$pattern" | grep -qE '(python.*venv|virtualenv|conda create)'; then
96
135
  PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
@@ -49,9 +49,37 @@ if command -v node >/dev/null 2>&1; then
49
49
  const basename = '$BASENAME';
50
50
  const banned = scope.bannedPatterns || {};
51
51
 
52
+ function globToRegex(pattern) {
53
+ let i = 0, re = '';
54
+ while (i < pattern.length) {
55
+ const c = pattern[i];
56
+ if (c === '*' && pattern[i+1] === '*') {
57
+ re += '.*'; i += 2;
58
+ if (pattern[i] === '/') i++;
59
+ } else if (c === '*') {
60
+ re += '[^/]*'; i++;
61
+ } else if (c === '?') {
62
+ re += '[^/]'; i++;
63
+ } else if (c === '[') {
64
+ const end = pattern.indexOf(']', i);
65
+ if (end > i) { re += pattern.slice(i, end + 1); i = end + 1; }
66
+ else { re += '\\\\['; i++; }
67
+ } else if (c === '{') {
68
+ const end = pattern.indexOf('}', i);
69
+ if (end > i) {
70
+ const alts = pattern.slice(i + 1, end).split(',').map(a => a.trim());
71
+ re += '(?:' + alts.join('|') + ')'; i = end + 1;
72
+ } else { re += '\\\\{'; i++; }
73
+ } else if ('.+^$|()'.includes(c)) {
74
+ re += '\\\\' + c; i++;
75
+ } else {
76
+ re += c; i++;
77
+ }
78
+ }
79
+ return new RegExp('^' + re + '$');
80
+ }
52
81
  function matchGlob(str, pattern) {
53
- const regex = new RegExp('^' + pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.') + '$');
54
- return regex.test(str);
82
+ return globToRegex(pattern).test(str);
55
83
  }
56
84
 
57
85
  // Check banned file patterns
@@ -51,9 +51,12 @@ BANNED_MODIFIERS=(
51
51
  # Convert filename to lowercase for checking
52
52
  FILENAME_LOWER=$(echo "$FILENAME" | tr '[:upper:]' '[:lower:]')
53
53
 
54
- # Check for banned modifiers
54
+ # Check for banned modifiers (word-boundary aware)
55
55
  for modifier in "${BANNED_MODIFIERS[@]}"; do
56
- if [[ "$FILENAME_LOWER" == *"$modifier"* ]]; then
56
+ # Match modifier preceded by start-of-string, hyphen, underscore, or dot
57
+ # and followed by end-of-string, hyphen, underscore, or dot
58
+ # Prevents false positives like "old" in "gold_oracle" or "new" in "renewable"
59
+ if [[ "$FILENAME_LOWER" =~ (^|[-_.])"$modifier"([-_.]|$) ]]; then
57
60
  # Special case: allow test files that follow conventions
58
61
  if [[ "$modifier" == "test-" ]] || [[ "$modifier" == "-test" ]] || [[ "$modifier" == "_test" ]]; then
59
62
  if [[ "$FILENAME_LOWER" =~ \.(test|spec)\.(js|ts|jsx|tsx|py|go|rs)$ ]]; then
@@ -44,6 +44,37 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
44
44
  LITE_CHECK=$(node -e "
45
45
  const fs = require('fs');
46
46
  const path = require('path');
47
+
48
+ function globToRegex(pattern) {
49
+ let i = 0, re = '';
50
+ while (i < pattern.length) {
51
+ const c = pattern[i];
52
+ if (c === '*' && pattern[i+1] === '*') {
53
+ re += '.*'; i += 2;
54
+ if (pattern[i] === '/') i++;
55
+ } else if (c === '*') {
56
+ re += '[^/]*'; i++;
57
+ } else if (c === '?') {
58
+ re += '[^/]'; i++;
59
+ } else if (c === '[') {
60
+ const end = pattern.indexOf(']', i);
61
+ if (end > i) { re += pattern.slice(i, end + 1); i = end + 1; }
62
+ else { re += '\\\\['; i++; }
63
+ } else if (c === '{') {
64
+ const end = pattern.indexOf('}', i);
65
+ if (end > i) {
66
+ const alts = pattern.slice(i + 1, end).split(',').map(a => a.trim());
67
+ re += '(?:' + alts.join('|') + ')'; i = end + 1;
68
+ } else { re += '\\\\{'; i++; }
69
+ } else if ('.+^$|()'.includes(c)) {
70
+ re += '\\\\' + c; i++;
71
+ } else {
72
+ re += c; i++;
73
+ }
74
+ }
75
+ return new RegExp(re);
76
+ }
77
+
47
78
  try {
48
79
  const scope = JSON.parse(fs.readFileSync('$SCOPE_FILE', 'utf8'));
49
80
  const filePath = '$REL_PATH';
@@ -54,7 +85,7 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
54
85
  const basename = path.basename(filePath);
55
86
  const bannedFiles = banned.files || [];
56
87
  for (const pattern of bannedFiles) {
57
- const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
88
+ const regex = globToRegex(pattern);
58
89
  if (regex.test(basename)) {
59
90
  console.log('banned:' + pattern);
60
91
  process.exit(0);
@@ -64,7 +95,7 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
64
95
  // Check banned doc patterns
65
96
  const bannedDocs = banned.docs || [];
66
97
  for (const pattern of bannedDocs) {
67
- const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
98
+ const regex = globToRegex(pattern);
68
99
  if (regex.test(basename)) {
69
100
  console.log('banned:' + pattern);
70
101
  process.exit(0);
@@ -129,6 +160,37 @@ if command -v node >/dev/null 2>&1; then
129
160
  const fs = require('fs');
130
161
  const path = require('path');
131
162
 
163
+ // Convert glob pattern to regex, handling **, *, ?, [abc], {a,b}
164
+ function globToRegex(pattern) {
165
+ let i = 0, re = '';
166
+ while (i < pattern.length) {
167
+ const c = pattern[i];
168
+ if (c === '*' && pattern[i+1] === '*') {
169
+ re += '.*'; i += 2;
170
+ if (pattern[i] === '/') i++; // skip trailing slash after **
171
+ } else if (c === '*') {
172
+ re += '[^/]*'; i++;
173
+ } else if (c === '?') {
174
+ re += '[^/]'; i++;
175
+ } else if (c === '[') {
176
+ const end = pattern.indexOf(']', i);
177
+ if (end > i) { re += pattern.slice(i, end + 1); i = end + 1; }
178
+ else { re += '\\\\['; i++; }
179
+ } else if (c === '{') {
180
+ const end = pattern.indexOf('}', i);
181
+ if (end > i) {
182
+ const alts = pattern.slice(i + 1, end).split(',').map(a => a.trim());
183
+ re += '(?:' + alts.join('|') + ')'; i = end + 1;
184
+ } else { re += '\\\\{'; i++; }
185
+ } else if ('.+^$|()'.includes(c)) {
186
+ re += '\\\\' + c; i++;
187
+ } else {
188
+ re += c; i++;
189
+ }
190
+ }
191
+ return new RegExp(re);
192
+ }
193
+
132
194
  try {
133
195
  const filePath = '$REL_PATH';
134
196
 
@@ -177,7 +239,7 @@ if command -v node >/dev/null 2>&1; then
177
239
  // Check scope.out across ALL active specs — any match blocks
178
240
  for (const { source, spec } of specs) {
179
241
  for (const pattern of (spec.scope?.out || [])) {
180
- const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
242
+ const regex = globToRegex(pattern);
181
243
  if (regex.test(filePath)) {
182
244
  console.log('out_of_scope:' + source + ':' + pattern);
183
245
  process.exit(0);
@@ -190,7 +252,7 @@ if command -v node >/dev/null 2>&1; then
190
252
  if (allInScope.length > 0) {
191
253
  let found = false;
192
254
  for (const pattern of allInScope) {
193
- const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
255
+ const regex = globToRegex(pattern);
194
256
  if (regex.test(filePath)) {
195
257
  found = true;
196
258
  break;
@@ -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"]
@@ -65,13 +65,22 @@ if [[ "$WT_COUNT" -le 0 ]] 2>/dev/null; then
65
65
  exit 0
66
66
  fi
67
67
 
68
- # Allow edits to .claude/ configuration (hooks, settings, rules)
68
+ # Allow edits to configuration and documentation (benign, no merge conflict risk)
69
69
  if [[ -n "$FILE_PATH" ]]; then
70
70
  case "$FILE_PATH" in
71
71
  */.claude/*|*/.caws/*) exit 0 ;;
72
+ */docs/*) exit 0 ;;
72
73
  esac
73
74
  fi
74
75
 
76
+ # Allow edits during an active merge (conflict resolution).
77
+ # The worktree-isolation rules explicitly permit merge commits on the base branch.
78
+ # Conflict resolution requires Write/Edit on the conflicted files.
79
+ MERGE_HEAD_PATH=$(cd "$AGENT_DIR" && git rev-parse --git-dir 2>/dev/null || echo ".git")
80
+ if [[ -f "$MERGE_HEAD_PATH/MERGE_HEAD" ]]; then
81
+ exit 0
82
+ fi
83
+
75
84
  # Block: we're on the base branch with active worktrees
76
85
  echo "BLOCKED: Cannot write/edit files on '$CURRENT_BRANCH' while $WT_COUNT worktree(s) are active: $WT_NAMES" >&2
77
86
  echo "" >&2
@@ -81,4 +90,7 @@ echo " To create a new worktree: caws worktree create <name>" >&2
81
90
  echo "" >&2
82
91
  echo "Do NOT make changes on main and create a worktree retroactively." >&2
83
92
  echo "The worktree must exist BEFORE you start making changes." >&2
93
+ echo "" >&2
94
+ echo "If you are merging a worktree branch, use: caws worktree merge <name>" >&2
95
+ echo "Or start the merge first (git merge --no-ff <branch>), then resolve conflicts." >&2
84
96
  exit 2
@@ -9,29 +9,62 @@ When multiple agents are working on this project, each agent MUST work in its ow
9
9
 
10
10
  ## Before starting work
11
11
 
12
- 1. Check if worktrees exist: look for `.caws/worktrees.json` or `.caws/parallel.json`
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
27
+ - Destroying another agent's worktree -- even with `--force`. If you did not create it, do not destroy it. Period.
24
28
 
25
29
  ## Merging worktree branches back to base
26
30
 
27
31
  Merge commits ARE allowed on the base branch while other worktrees are active. This lets you incrementally merge completed work without waiting for all agents to finish.
28
32
 
33
+ ### Recommended: use `caws worktree merge`
34
+
35
+ The `merge` command handles the full sequence (conflict check, destroy, merge, cleanup):
36
+
37
+ ```bash
38
+ # Preview conflicts before merging
39
+ caws worktree merge <name> --dry-run
40
+
41
+ # Merge (destroys worktree, merges branch, deletes branch)
42
+ caws worktree merge <name>
43
+
44
+ # Merge with custom commit message
45
+ caws worktree merge <name> --message "merge(worktree): description of changes"
46
+ ```
47
+
48
+ ### Manual merge (if you need more control)
49
+
29
50
  1. Destroy the worktree first: `caws worktree destroy <name>`
30
51
  2. Switch to the base branch: `git checkout main`
31
52
  3. Merge with: `git merge --no-ff <worktree-branch>`
32
53
  4. The commit-msg hook enforces the `merge(worktree): <description>` format for non-FF merges
33
54
  5. For manual merge commits: `git commit -m "merge(worktree): integrate scenarios work"`
34
55
 
56
+ ### Conflict resolution during merge
57
+
58
+ The write guard allows edits on the base branch while a merge is in progress (MERGE_HEAD exists). This lets you resolve merge conflicts without needing to abort and retry. After resolving, commit with the `merge(worktree):` format.
59
+
60
+ ## What the write guard allows on the base branch
61
+
62
+ Even when worktrees are active, the following edits are allowed on the base branch:
63
+
64
+ - `.claude/` and `.caws/` configuration files
65
+ - `docs/` directory (documentation changes are benign)
66
+ - Any file while a merge is in progress (conflict resolution)
67
+
35
68
  ## Virtual environment in worktrees
36
69
 
37
70
  Do NOT create a new virtual environment in your worktree. Use the main repo's venv:
@@ -46,6 +79,5 @@ If your project uses `.caws/scope.json`, the `designatedVenvPath` field specifie
46
79
 
47
80
  1. Commit all changes to your worktree branch
48
81
  2. Run tests in your worktree to verify
49
- 3. Destroy your worktree with `caws worktree destroy <name>`
50
- 4. Merge your branch to base: `git merge --no-ff <branch>` (uses `merge(worktree):` format)
51
- 5. Delete the branch if no longer needed: `git branch -d <branch>`
82
+ 3. Merge: `caws worktree merge <name>` (handles destroy + merge + branch cleanup)
83
+ 4. Or manually: destroy worktree, then `git merge --no-ff <branch>`, then delete branch
@@ -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)