@paths.design/caws-cli 8.3.0 → 9.1.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 (56) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/parallel.d.ts +7 -0
  3. package/dist/commands/parallel.d.ts.map +1 -0
  4. package/dist/commands/parallel.js +238 -0
  5. package/dist/commands/session.d.ts +7 -0
  6. package/dist/commands/session.d.ts.map +1 -0
  7. package/dist/commands/specs.d.ts +6 -0
  8. package/dist/commands/specs.d.ts.map +1 -1
  9. package/dist/commands/specs.js +55 -2
  10. package/dist/commands/status.d.ts.map +1 -1
  11. package/dist/commands/status.js +13 -3
  12. package/dist/commands/tutorial.js +0 -2
  13. package/dist/commands/waivers.d.ts.map +1 -1
  14. package/dist/constants/spec-types.d.ts +52 -0
  15. package/dist/constants/spec-types.d.ts.map +1 -1
  16. package/dist/constants/spec-types.js +25 -2
  17. package/dist/index.js +43 -2
  18. package/dist/parallel/parallel-manager.d.ts +67 -0
  19. package/dist/parallel/parallel-manager.d.ts.map +1 -0
  20. package/dist/parallel/parallel-manager.js +440 -0
  21. package/dist/scaffold/claude-hooks.d.ts.map +1 -1
  22. package/dist/scaffold/claude-hooks.js +78 -2
  23. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  24. package/dist/scaffold/git-hooks.js +237 -73
  25. package/dist/scaffold/index.d.ts.map +1 -1
  26. package/dist/session/session-manager.d.ts +94 -0
  27. package/dist/session/session-manager.d.ts.map +1 -0
  28. package/dist/session/session-manager.js +14 -0
  29. package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
  30. package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
  31. package/dist/templates/.claude/hooks/session-log.sh +528 -0
  32. package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  33. package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
  34. package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  35. package/dist/templates/.claude/rules/git-safety.md +26 -0
  36. package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
  37. package/dist/templates/.claude/settings.json +15 -0
  38. package/dist/utils/gitignore-updater.d.ts +1 -1
  39. package/dist/utils/gitignore-updater.d.ts.map +1 -1
  40. package/dist/utils/gitignore-updater.js +3 -0
  41. package/dist/utils/ide-detection.d.ts +89 -0
  42. package/dist/utils/ide-detection.d.ts.map +1 -0
  43. package/dist/validation/spec-validation.d.ts.map +1 -1
  44. package/dist/validation/spec-validation.js +16 -0
  45. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  46. package/dist/worktree/worktree-manager.js +31 -21
  47. package/package.json +2 -2
  48. package/templates/.claude/hooks/scope-guard.sh +67 -21
  49. package/templates/.claude/hooks/session-caws-status.sh +117 -0
  50. package/templates/.claude/hooks/session-log.sh +528 -0
  51. package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
  52. package/templates/.claude/hooks/worktree-guard.sh +207 -0
  53. package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
  54. package/templates/.claude/rules/git-safety.md +26 -0
  55. package/templates/.claude/rules/worktree-isolation.md +51 -0
  56. package/templates/.claude/settings.json +15 -0
@@ -1,6 +1,7 @@
1
1
  #!/bin/bash
2
2
  # CAWS Scope Guard Hook for Claude Code
3
- # Validates file edits against the working spec's scope boundaries
3
+ # Validates file edits against scope boundaries from working-spec + feature specs
4
+ # Specs with terminal status (completed, closed, archived) are skipped
4
5
  # @author @darianrosebrook
5
6
 
6
7
  set -euo pipefail
@@ -25,8 +26,8 @@ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
25
26
  SPEC_FILE="$PROJECT_DIR/.caws/working-spec.yaml"
26
27
  SCOPE_FILE="$PROJECT_DIR/.caws/scope.json"
27
28
 
28
- # Check if spec file or scope.json exists
29
- if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]]; then
29
+ # Check if any spec infrastructure exists
30
+ if [[ ! -f "$SPEC_FILE" ]] && [[ ! -f "$SCOPE_FILE" ]] && [[ ! -d "$PROJECT_DIR/.caws/specs" ]]; then
30
31
  exit 0
31
32
  fi
32
33
 
@@ -119,7 +120,9 @@ if [[ ! -f "$SPEC_FILE" ]] && [[ -f "$SCOPE_FILE" ]]; then
119
120
  fi
120
121
  fi
121
122
 
122
- # Use Node.js to parse YAML and check scope
123
+ # Use Node.js to parse YAML and check scope across working spec + active feature specs
124
+ SPECS_DIR="$PROJECT_DIR/.caws/specs"
125
+
123
126
  if command -v node >/dev/null 2>&1; then
124
127
  SCOPE_CHECK=$(node -e "
125
128
  const yaml = require('js-yaml');
@@ -127,26 +130,67 @@ if command -v node >/dev/null 2>&1; then
127
130
  const path = require('path');
128
131
 
129
132
  try {
130
- const spec = yaml.load(fs.readFileSync('$SPEC_FILE', 'utf8'));
131
133
  const filePath = '$REL_PATH';
132
134
 
133
- // Check if file is explicitly out of scope
134
- const outOfScope = spec.scope?.out || [];
135
- for (const pattern of outOfScope) {
136
- // Simple glob-like matching
137
- const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
138
- if (regex.test(filePath)) {
139
- console.log('out_of_scope:' + pattern);
140
- process.exit(0);
135
+ // Terminal statuses: specs that are done scope no longer enforced
136
+ const TERMINAL = new Set(['completed', 'closed', 'archived']);
137
+
138
+ // Smart allowlist: root-level files, .caws/, .claude/ always pass
139
+ if (!filePath.includes('/') || filePath.startsWith('.caws/') || filePath.startsWith('.claude/')) {
140
+ console.log('in_scope');
141
+ process.exit(0);
142
+ }
143
+
144
+ // Collect all active specs (working-spec + feature specs)
145
+ const specs = [];
146
+
147
+ // Load working-spec.yaml if present
148
+ const mainSpec = '$SPEC_FILE';
149
+ if (fs.existsSync(mainSpec)) {
150
+ try {
151
+ const s = yaml.load(fs.readFileSync(mainSpec, 'utf8'));
152
+ if (s && !TERMINAL.has(s.status)) {
153
+ specs.push({ source: 'working-spec', spec: s });
154
+ }
155
+ } catch (_) {}
156
+ }
157
+
158
+ // Load feature specs from .caws/specs/
159
+ const specsDir = '$SPECS_DIR';
160
+ if (fs.existsSync(specsDir)) {
161
+ for (const f of fs.readdirSync(specsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {
162
+ try {
163
+ const s = yaml.load(fs.readFileSync(path.join(specsDir, f), 'utf8'));
164
+ if (s && !TERMINAL.has(s.status)) {
165
+ specs.push({ source: f, spec: s });
166
+ }
167
+ } catch (_) {}
141
168
  }
142
169
  }
143
170
 
144
- // Check if file is in scope (if scope is explicitly defined)
145
- const inScope = spec.scope?.in || [];
146
- if (inScope.length > 0) {
171
+ // No active specs allow everything
172
+ if (specs.length === 0) {
173
+ console.log('in_scope');
174
+ process.exit(0);
175
+ }
176
+
177
+ // Check scope.out across ALL active specs — any match blocks
178
+ for (const { source, spec } of specs) {
179
+ for (const pattern of (spec.scope?.out || [])) {
180
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
181
+ if (regex.test(filePath)) {
182
+ console.log('out_of_scope:' + source + ':' + pattern);
183
+ process.exit(0);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Union all scope.in patterns — file must match at least one
189
+ const allInScope = specs.flatMap(({ spec }) => spec.scope?.in || []);
190
+ if (allInScope.length > 0) {
147
191
  let found = false;
148
- for (const pattern of inScope) {
149
- const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
192
+ for (const pattern of allInScope) {
193
+ const regex = new RegExp(pattern.replace(/\\*/g, '.*').replace(/\\?/g, '.'));
150
194
  if (regex.test(filePath)) {
151
195
  found = true;
152
196
  break;
@@ -165,12 +209,14 @@ if command -v node >/dev/null 2>&1; then
165
209
  " 2>&1)
166
210
 
167
211
  if [[ "$SCOPE_CHECK" == out_of_scope:* ]]; then
168
- PATTERN="${SCOPE_CHECK#out_of_scope:}"
212
+ DETAIL="${SCOPE_CHECK#out_of_scope:}"
213
+ SOURCE="${DETAIL%%:*}"
214
+ PATTERN="${DETAIL#*:}"
169
215
  echo '{
170
216
  "hookSpecificOutput": {
171
217
  "hookEventName": "PreToolUse",
172
218
  "permissionDecision": "ask",
173
- "permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in the working spec (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
219
+ "permissionDecisionReason": "This file ('"$REL_PATH"') is marked as out-of-scope in '"$SOURCE"' (pattern: '"$PATTERN"'). Editing it may cause scope creep. Please confirm this edit is intentional."
174
220
  }
175
221
  }'
176
222
  exit 0
@@ -181,7 +227,7 @@ if command -v node >/dev/null 2>&1; then
181
227
  "hookSpecificOutput": {
182
228
  "hookEventName": "PreToolUse",
183
229
  "permissionDecision": "ask",
184
- "permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of the working spec. Editing it may cause scope creep. Please confirm this edit is intentional."
230
+ "permissionDecisionReason": "This file ('"$REL_PATH"') is not in the defined scope of any active spec. Editing it may cause scope creep. Please confirm this edit is intentional."
185
231
  }
186
232
  }'
187
233
  exit 0
@@ -0,0 +1,117 @@
1
+ #!/bin/bash
2
+ # CAWS Session Status Hook for Claude Code
3
+ # Reports project state at session start with worktree warnings
4
+ # @author @darianrosebrook
5
+
6
+ set -euo pipefail
7
+
8
+ # Read stdin (required by hook protocol)
9
+ INPUT=$(cat)
10
+
11
+ # Only run for session-start events
12
+ EVENT_TYPE="${1:-}"
13
+ if [ "$EVENT_TYPE" != "session-start" ]; then
14
+ exit 0
15
+ fi
16
+
17
+ # Check if this is a CAWS project
18
+ if [ ! -d "${CLAUDE_PROJECT_DIR:-.}/.caws" ]; then
19
+ exit 0
20
+ fi
21
+
22
+ cd "${CLAUDE_PROJECT_DIR:-.}"
23
+
24
+ # --- Resolve main repo root ---
25
+ CAWS_ROOT="."
26
+ if command -v git >/dev/null 2>&1; then
27
+ _GIT_COMMON=$(git rev-parse --git-common-dir 2>/dev/null || echo ".git")
28
+ if [ "$_GIT_COMMON" != ".git" ]; then
29
+ _CANDIDATE=$(cd "$_GIT_COMMON/.." 2>/dev/null && pwd || echo "")
30
+ if [ -n "$_CANDIDATE" ] && [ -d "$_CANDIDATE/.caws" ]; then
31
+ CAWS_ROOT="$_CANDIDATE"
32
+ fi
33
+ fi
34
+ fi
35
+
36
+ # --- Active worktree warning ---
37
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
38
+
39
+ if [ -f "$CAWS_ROOT/.caws/worktrees.json" ] && command -v node >/dev/null 2>&1; then
40
+ WT_INFO=$(node -e "
41
+ try {
42
+ var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
43
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
44
+ if (active.length > 0) {
45
+ var names = active.map(function(w) { return w.name + ' (' + w.branch + ')'; });
46
+ console.log(active.length + ':' + names.join(', '));
47
+ } else {
48
+ console.log('0:');
49
+ }
50
+ } catch(e) { console.log('0:'); }
51
+ " 2>/dev/null || echo "0:")
52
+
53
+ WT_COUNT=$(echo "$WT_INFO" | cut -d: -f1)
54
+ WT_NAMES=$(echo "$WT_INFO" | cut -d: -f2)
55
+
56
+ if [ "$WT_COUNT" -gt 0 ] 2>/dev/null; then
57
+ # Check if the agent is already in a worktree (not on the base branch)
58
+ BASE_BRANCH=$(node -e "
59
+ try {
60
+ var reg = JSON.parse(require('fs').readFileSync('$CAWS_ROOT/.caws/worktrees.json', 'utf8'));
61
+ var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
62
+ if (active.length > 0) console.log(active[0].baseBranch || '');
63
+ else console.log('');
64
+ } catch(e) { console.log(''); }
65
+ " 2>/dev/null || echo "")
66
+
67
+ echo ""
68
+ echo "================================================================"
69
+ echo " ACTIVE WORKTREES DETECTED: $WT_COUNT worktree(s)"
70
+ echo " $WT_NAMES"
71
+ echo "================================================================"
72
+
73
+ if [ -n "$BASE_BRANCH" ] && [ "$CURRENT_BRANCH" = "$BASE_BRANCH" ]; then
74
+ echo ""
75
+ echo " You MUST work in a worktree, not on $CURRENT_BRANCH."
76
+ echo ""
77
+ echo " If a worktree was created for your task:"
78
+ echo " cd $CAWS_ROOT/.caws/worktrees/<name>/"
79
+ echo ""
80
+ echo " If you need a new worktree:"
81
+ echo " caws worktree create <name>"
82
+ echo ""
83
+ echo " The only operations allowed on $CURRENT_BRANCH are:"
84
+ echo " - git merge --no-ff <branch> (merge completed worktree work)"
85
+ echo " - Commits with message: merge(worktree): <description>"
86
+ echo " - Commits with message: wip(checkpoint): <description>"
87
+ echo " (for committing prior-session dirty files)"
88
+ echo ""
89
+ echo " Writing or editing files on $CURRENT_BRANCH will be BLOCKED"
90
+ echo " by the PreToolUse hook while worktrees are active."
91
+ else
92
+ echo ""
93
+ echo " You are on branch '$CURRENT_BRANCH' (worktree). Good."
94
+ echo " Other active worktrees: $WT_NAMES"
95
+ fi
96
+ echo "================================================================"
97
+ echo ""
98
+ fi
99
+ fi
100
+
101
+ # Use caws session briefing for structured output
102
+ if command -v caws &>/dev/null; then
103
+ caws session briefing 2>/dev/null || {
104
+ echo "--- CAWS Session Briefing (fallback) ---"
105
+ HEAD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
106
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
107
+ DIRTY_COUNT=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
108
+ echo "Git: ${BRANCH} @ ${HEAD_SHA} (${DIRTY_COUNT} dirty files)"
109
+ if [ "$DIRTY_COUNT" -gt 0 ]; then
110
+ echo "WARNING: Working tree has uncommitted changes from a prior session."
111
+ echo "Classify and commit or stash them before starting new work."
112
+ fi
113
+ echo "--- End CAWS Briefing ---"
114
+ }
115
+ fi
116
+
117
+ exit 0