@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.
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/parallel.d.ts +7 -0
- package/dist/commands/parallel.d.ts.map +1 -0
- package/dist/commands/parallel.js +238 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/specs.d.ts +6 -0
- package/dist/commands/specs.d.ts.map +1 -1
- package/dist/commands/specs.js +55 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -3
- package/dist/commands/tutorial.js +0 -2
- package/dist/commands/waivers.d.ts.map +1 -1
- package/dist/constants/spec-types.d.ts +52 -0
- package/dist/constants/spec-types.d.ts.map +1 -1
- package/dist/constants/spec-types.js +25 -2
- package/dist/index.js +43 -2
- package/dist/parallel/parallel-manager.d.ts +67 -0
- package/dist/parallel/parallel-manager.d.ts.map +1 -0
- package/dist/parallel/parallel-manager.js +440 -0
- package/dist/scaffold/claude-hooks.d.ts.map +1 -1
- package/dist/scaffold/claude-hooks.js +78 -2
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +237 -73
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/session/session-manager.d.ts +94 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +14 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +67 -21
- package/dist/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/dist/templates/.claude/hooks/session-log.sh +528 -0
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/dist/templates/.claude/rules/git-safety.md +26 -0
- package/dist/templates/.claude/rules/worktree-isolation.md +51 -0
- package/dist/templates/.claude/settings.json +15 -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 +3 -0
- package/dist/utils/ide-detection.d.ts +89 -0
- package/dist/utils/ide-detection.d.ts.map +1 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +16 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -1
- package/dist/worktree/worktree-manager.js +31 -21
- package/package.json +2 -2
- package/templates/.claude/hooks/scope-guard.sh +67 -21
- package/templates/.claude/hooks/session-caws-status.sh +117 -0
- package/templates/.claude/hooks/session-log.sh +528 -0
- package/templates/.claude/hooks/stop-worktree-check.sh +46 -0
- package/templates/.claude/hooks/worktree-guard.sh +207 -0
- package/templates/.claude/hooks/worktree-write-guard.sh +84 -0
- package/templates/.claude/rules/git-safety.md +26 -0
- package/templates/.claude/rules/worktree-isolation.md +51 -0
- package/templates/.claude/settings.json +15 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Worktree Safety Guard for Claude Code
|
|
3
|
+
# Blocks dangerous operations when parallel worktrees are active
|
|
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
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
14
|
+
|
|
15
|
+
# Only check Bash tool
|
|
16
|
+
if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# --- Resolve main repo root ---
|
|
21
|
+
# When running inside a worktree, CLAUDE_PROJECT_DIR points to the
|
|
22
|
+
# worktree directory, but .caws/worktrees.json only exists in the main
|
|
23
|
+
# repo. Use git's common dir to find the true repo root.
|
|
24
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
25
|
+
|
|
26
|
+
if command -v git >/dev/null 2>&1; then
|
|
27
|
+
GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
28
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
|
|
29
|
+
# Inside a worktree: --git-common-dir returns the main repo's .git path
|
|
30
|
+
# (e.g., /path/to/repo/.git or /path/to/repo/.git/worktrees/<name>/..)
|
|
31
|
+
CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
|
|
32
|
+
if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
|
|
33
|
+
PROJECT_DIR="$CANDIDATE"
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# --- Gap 2: Block sparse checkout before the git-only filter ---
|
|
39
|
+
# This must run before the "only check git commands" early-exit
|
|
40
|
+
if echo "$COMMAND" | grep -qE 'caws\s+(worktree\s+create|parallel\s+setup).*--scope'; then
|
|
41
|
+
echo "BLOCKED: --scope (sparse checkout) is not allowed." >&2
|
|
42
|
+
echo "Sparse checkout breaks cross-module imports in most projects." >&2
|
|
43
|
+
echo "Use full worktrees without --scope. Scope enforcement comes from" >&2
|
|
44
|
+
echo "CAWS feature specs and lane discipline, not from hiding files." >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# --- Gap 5: Block cross-boundary file copies ---
|
|
49
|
+
WORKTREE_BASE="$PROJECT_DIR/.caws/worktrees"
|
|
50
|
+
if [[ -d "$WORKTREE_BASE" ]]; then
|
|
51
|
+
if echo "$COMMAND" | grep -qE '\b(cp|mv)\b'; then
|
|
52
|
+
if echo "$COMMAND" | grep -qF ".caws/worktrees/" || echo "$COMMAND" | grep -qF "$WORKTREE_BASE"; then
|
|
53
|
+
# Check if the command references both a worktree path and the main repo
|
|
54
|
+
HAS_WT_PATH=false
|
|
55
|
+
HAS_MAIN_PATH=false
|
|
56
|
+
if echo "$COMMAND" | grep -qE '\.caws/worktrees/|'"$(echo "$WORKTREE_BASE" | sed 's/[\/&]/\\&/g')"''; then
|
|
57
|
+
HAS_WT_PATH=true
|
|
58
|
+
fi
|
|
59
|
+
# Check if destination/source is outside the worktree
|
|
60
|
+
if echo "$COMMAND" | grep -qE "(^|\s)$PROJECT_DIR/[^.]|core/|src/|tests/|packages/" && [[ "$HAS_WT_PATH" == "true" ]]; then
|
|
61
|
+
HAS_MAIN_PATH=true
|
|
62
|
+
fi
|
|
63
|
+
if [[ "$HAS_WT_PATH" == "true" ]] && [[ "$HAS_MAIN_PATH" == "true" ]]; then
|
|
64
|
+
echo "BLOCKED: Copying files between a worktree and the main repo is forbidden." >&2
|
|
65
|
+
echo "This bypasses worktree isolation. Work entirely within your worktree." >&2
|
|
66
|
+
echo "If tests need the main repo's venv, activate it with:" >&2
|
|
67
|
+
echo " source $PROJECT_DIR/.venv/bin/activate" >&2
|
|
68
|
+
exit 2
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Only check git commands from here on
|
|
75
|
+
if ! echo "$COMMAND" | grep -qE '(^|\s|&&|\|)git\s'; then
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- Determine if worktrees are active ---
|
|
80
|
+
WORKTREES_ACTIVE=false
|
|
81
|
+
PARALLEL_BASE=""
|
|
82
|
+
|
|
83
|
+
# Check .caws/parallel.json
|
|
84
|
+
if [[ -f "$PROJECT_DIR/.caws/parallel.json" ]] && command -v node >/dev/null 2>&1; then
|
|
85
|
+
PARALLEL_INFO=$(node -e "
|
|
86
|
+
try {
|
|
87
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/parallel.json', 'utf8'));
|
|
88
|
+
var agents = (reg.agents || []).length;
|
|
89
|
+
console.log(agents + ':' + (reg.baseBranch || ''));
|
|
90
|
+
} catch(e) { console.log('0:'); }
|
|
91
|
+
" 2>/dev/null || echo "0:")
|
|
92
|
+
|
|
93
|
+
AGENT_COUNT=$(echo "$PARALLEL_INFO" | cut -d: -f1)
|
|
94
|
+
PARALLEL_BASE=$(echo "$PARALLEL_INFO" | cut -d: -f2)
|
|
95
|
+
|
|
96
|
+
if [[ "$AGENT_COUNT" -gt 0 ]] 2>/dev/null; then
|
|
97
|
+
WORKTREES_ACTIVE=true
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Check .caws/worktrees.json
|
|
102
|
+
if [[ "$WORKTREES_ACTIVE" != "true" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
|
|
103
|
+
ACTIVE_COUNT=$(node -e "
|
|
104
|
+
try {
|
|
105
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
106
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
107
|
+
console.log(active.length);
|
|
108
|
+
} catch(e) { console.log('0'); }
|
|
109
|
+
" 2>/dev/null || echo "0")
|
|
110
|
+
|
|
111
|
+
if [[ "$ACTIVE_COUNT" -gt 0 ]] 2>/dev/null; then
|
|
112
|
+
WORKTREES_ACTIVE=true
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# If no worktrees are active, allow everything
|
|
117
|
+
if [[ "$WORKTREES_ACTIVE" != "true" ]]; then
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# --- Block dangerous git operations when worktrees are active ---
|
|
122
|
+
|
|
123
|
+
# Block git commit --amend
|
|
124
|
+
if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*--amend'; then
|
|
125
|
+
echo "BLOCKED: git commit --amend is not allowed while worktrees are active." >&2
|
|
126
|
+
echo "Amending commits risks rewriting another agent's work." >&2
|
|
127
|
+
echo "Create a new commit instead." >&2
|
|
128
|
+
exit 2
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Block git stash (shared across worktrees)
|
|
132
|
+
if echo "$COMMAND" | grep -qE 'git\s+stash' && ! echo "$COMMAND" | grep -qE 'git\s+stash\s+list'; then
|
|
133
|
+
echo "BLOCKED: git stash is not allowed while worktrees are active." >&2
|
|
134
|
+
echo "Stash is shared across all worktrees and can capture or destroy another agent's work." >&2
|
|
135
|
+
echo "Commit your changes to your branch instead." >&2
|
|
136
|
+
exit 2
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Block git reset --hard
|
|
140
|
+
if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then
|
|
141
|
+
echo "BLOCKED: git reset --hard is not allowed while worktrees are active." >&2
|
|
142
|
+
echo "This could discard work that other agents depend on." >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Block git push --force
|
|
147
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(--force|-f\s)'; then
|
|
148
|
+
echo "BLOCKED: Force push is not allowed while worktrees are active." >&2
|
|
149
|
+
echo "This could rewrite history that other agents have based work on." >&2
|
|
150
|
+
exit 2
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# --- Base branch protections ---
|
|
154
|
+
# Use the agent's actual working directory (CLAUDE_PROJECT_DIR), not the resolved
|
|
155
|
+
# main repo root (PROJECT_DIR). In a worktree, PROJECT_DIR points to the main repo
|
|
156
|
+
# (to find .caws/worktrees.json), but the agent's branch is in CLAUDE_PROJECT_DIR.
|
|
157
|
+
AGENT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
158
|
+
CURRENT_BRANCH=$(cd "$AGENT_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
159
|
+
|
|
160
|
+
# Determine the base branch to protect
|
|
161
|
+
BASE_BRANCH="$PARALLEL_BASE"
|
|
162
|
+
if [[ -z "$BASE_BRANCH" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
|
|
163
|
+
BASE_BRANCH=$(node -e "
|
|
164
|
+
try {
|
|
165
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
166
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) { return w.status === 'active'; });
|
|
167
|
+
if (active.length > 0) console.log(active[0].baseBranch || '');
|
|
168
|
+
else console.log('');
|
|
169
|
+
} catch(e) { console.log(''); }
|
|
170
|
+
" 2>/dev/null || echo "")
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
if [[ -n "$BASE_BRANCH" ]] && [[ "$CURRENT_BRANCH" == "$BASE_BRANCH" ]]; then
|
|
174
|
+
# Block push from base branch
|
|
175
|
+
if echo "$COMMAND" | grep -qE 'git\s+push'; then
|
|
176
|
+
echo "BLOCKED: Pushing from the base branch ($BASE_BRANCH) while worktrees are active." >&2
|
|
177
|
+
echo "You should be working in a worktree, not on the base branch." >&2
|
|
178
|
+
echo "Use: cd .caws/worktrees/<name>/" >&2
|
|
179
|
+
exit 2
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Allow git merge into base branch (merging completed worktree branches back)
|
|
183
|
+
# The commit-msg hook enforces the merge(worktree): message format
|
|
184
|
+
if echo "$COMMAND" | grep -qE 'git\s+merge\b'; then
|
|
185
|
+
echo '{
|
|
186
|
+
"hookSpecificOutput": {
|
|
187
|
+
"hookEventName": "PreToolUse",
|
|
188
|
+
"additionalContext": "Merging into base branch ('"$BASE_BRANCH"') while worktrees are active. The commit-msg hook will enforce the merge(worktree): message format. Make sure the worktree for this branch has been destroyed first."
|
|
189
|
+
}
|
|
190
|
+
}'
|
|
191
|
+
exit 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# Warn (but don't block) commits on base branch — the pre-commit + commit-msg hooks handle blocking
|
|
195
|
+
if echo "$COMMAND" | grep -qE 'git\s+commit\b' && ! echo "$COMMAND" | grep -qE '--amend'; then
|
|
196
|
+
echo '{
|
|
197
|
+
"hookSpecificOutput": {
|
|
198
|
+
"hookEventName": "PreToolUse",
|
|
199
|
+
"additionalContext": "WARNING: You are committing to the base branch ('"$BASE_BRANCH"') while worktrees are active. Only merge commits with the format merge(worktree): <description> are allowed. The pre-commit hook will block direct commits."
|
|
200
|
+
}
|
|
201
|
+
}'
|
|
202
|
+
exit 0
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# Allow the command
|
|
207
|
+
exit 0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Worktree Write Guard for Claude Code
|
|
3
|
+
# Blocks Write/Edit on the base branch while worktrees are active.
|
|
4
|
+
# This prevents agents from modifying files on main and then trying to
|
|
5
|
+
# create worktrees retroactively to commit them.
|
|
6
|
+
# @author @darianrosebrook
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# Read JSON input from Claude Code
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
|
|
13
|
+
# Extract tool info
|
|
14
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
15
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
16
|
+
|
|
17
|
+
# Only check Write and Edit tools
|
|
18
|
+
case "$TOOL_NAME" in
|
|
19
|
+
Write|Edit) ;;
|
|
20
|
+
*) exit 0 ;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
23
|
+
# --- Resolve main repo root ---
|
|
24
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
25
|
+
|
|
26
|
+
if command -v git >/dev/null 2>&1; then
|
|
27
|
+
GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
28
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
|
|
29
|
+
CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
|
|
30
|
+
if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
|
|
31
|
+
PROJECT_DIR="$CANDIDATE"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# --- Check for active worktrees ---
|
|
37
|
+
if [[ ! -f "$PROJECT_DIR/.caws/worktrees.json" ]]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Use the agent's actual working directory, not the resolved main repo root.
|
|
46
|
+
# In a worktree, PROJECT_DIR points to the main repo (to find .caws/worktrees.json),
|
|
47
|
+
# but the agent's branch is in CLAUDE_PROJECT_DIR.
|
|
48
|
+
AGENT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
49
|
+
CURRENT_BRANCH=$(cd "$AGENT_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
50
|
+
|
|
51
|
+
WT_INFO=$(node -e "
|
|
52
|
+
try {
|
|
53
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
54
|
+
var active = Object.values(reg.worktrees || {}).filter(function(w) {
|
|
55
|
+
return w.status === 'active' && w.baseBranch === '$CURRENT_BRANCH';
|
|
56
|
+
});
|
|
57
|
+
console.log(active.length + ':' + active.map(function(w) { return w.name; }).join(', '));
|
|
58
|
+
} catch(e) { console.log('0:'); }
|
|
59
|
+
" 2>/dev/null || echo "0:")
|
|
60
|
+
|
|
61
|
+
WT_COUNT=$(echo "$WT_INFO" | cut -d: -f1)
|
|
62
|
+
WT_NAMES=$(echo "$WT_INFO" | cut -d: -f2)
|
|
63
|
+
|
|
64
|
+
if [[ "$WT_COUNT" -le 0 ]] 2>/dev/null; then
|
|
65
|
+
exit 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# Allow edits to .claude/ configuration (hooks, settings, rules)
|
|
69
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
70
|
+
case "$FILE_PATH" in
|
|
71
|
+
*/.claude/*|*/.caws/*) exit 0 ;;
|
|
72
|
+
esac
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Block: we're on the base branch with active worktrees
|
|
76
|
+
echo "BLOCKED: Cannot write/edit files on '$CURRENT_BRANCH' while $WT_COUNT worktree(s) are active: $WT_NAMES" >&2
|
|
77
|
+
echo "" >&2
|
|
78
|
+
echo "You MUST work in a worktree, not on the base branch." >&2
|
|
79
|
+
echo " To use an existing worktree: cd $PROJECT_DIR/.caws/worktrees/<name>/" >&2
|
|
80
|
+
echo " To create a new worktree: caws worktree create <name>" >&2
|
|
81
|
+
echo "" >&2
|
|
82
|
+
echo "Do NOT make changes on main and create a worktree retroactively." >&2
|
|
83
|
+
echo "The worktree must exist BEFORE you start making changes." >&2
|
|
84
|
+
exit 2
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Git safety rules for all agents
|
|
3
|
+
globs:
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git Safety
|
|
7
|
+
|
|
8
|
+
## Commit discipline
|
|
9
|
+
|
|
10
|
+
- Commit after each logical unit of work (a module + its tests, a bugfix, a refactor pass)
|
|
11
|
+
- Use conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`, `test:`, `perf:`
|
|
12
|
+
- Never accumulate uncommitted changes across multiple unrelated concerns
|
|
13
|
+
- Never leave uncommitted changes at session end; commit as `wip(<scope>): <description>` if incomplete
|
|
14
|
+
|
|
15
|
+
## Forbidden operations
|
|
16
|
+
|
|
17
|
+
- `git push --force` or `git push -f` -- never rewrite remote history
|
|
18
|
+
- `git reset --hard` -- use `git stash` or `git checkout -- <file>` for targeted reverts (but not during parallel work)
|
|
19
|
+
- `git clean -f` -- may delete another agent's untracked files
|
|
20
|
+
- `git checkout .` or `git restore .` -- bulk discard is dangerous
|
|
21
|
+
|
|
22
|
+
## Branch hygiene
|
|
23
|
+
|
|
24
|
+
- Work on feature branches, not directly on main/master
|
|
25
|
+
- One concern per branch
|
|
26
|
+
- Delete branches after merging
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rules for safe multi-agent git worktree isolation
|
|
3
|
+
globs:
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multi-Agent Worktree Safety
|
|
7
|
+
|
|
8
|
+
When multiple agents are working on this project, each agent MUST work in its own git worktree. Never have two agents committing to the same branch.
|
|
9
|
+
|
|
10
|
+
## Before starting work
|
|
11
|
+
|
|
12
|
+
1. Check if worktrees exist: look for `.caws/worktrees.json` or `.caws/parallel.json`
|
|
13
|
+
2. If worktrees are active and you are on the base branch, switch to your assigned worktree
|
|
14
|
+
3. If no worktree exists for you, create one with `caws worktree create <name>` or `caws parallel setup <plan-file>`
|
|
15
|
+
|
|
16
|
+
## Forbidden operations when worktrees are active
|
|
17
|
+
|
|
18
|
+
- `git commit --amend` -- rewrites history that other agents depend on
|
|
19
|
+
- `git stash` / `git stash pop` -- stash is shared across all worktrees; using it can destroy another agent's uncommitted work
|
|
20
|
+
- `git reset --hard` -- discards work that other agents may depend on
|
|
21
|
+
- `git push --force` -- rewrites remote history
|
|
22
|
+
- Direct commits to the base branch -- only `merge(worktree):` and `wip(checkpoint):` formats are allowed
|
|
23
|
+
- Copying files between your worktree and the main repo directory -- defeats isolation
|
|
24
|
+
|
|
25
|
+
## Merging worktree branches back to base
|
|
26
|
+
|
|
27
|
+
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
|
+
|
|
29
|
+
1. Destroy the worktree first: `caws worktree destroy <name>`
|
|
30
|
+
2. Switch to the base branch: `git checkout main`
|
|
31
|
+
3. Merge with: `git merge --no-ff <worktree-branch>`
|
|
32
|
+
4. The commit-msg hook enforces the `merge(worktree): <description>` format for non-FF merges
|
|
33
|
+
5. For manual merge commits: `git commit -m "merge(worktree): integrate scenarios work"`
|
|
34
|
+
|
|
35
|
+
## Virtual environment in worktrees
|
|
36
|
+
|
|
37
|
+
Do NOT create a new virtual environment in your worktree. Use the main repo's venv:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
source <main-repo-path>/.venv/bin/activate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If your project uses `.caws/scope.json`, the `designatedVenvPath` field specifies the correct venv location.
|
|
44
|
+
|
|
45
|
+
## When your work is done
|
|
46
|
+
|
|
47
|
+
1. Commit all changes to your worktree branch
|
|
48
|
+
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>`
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh",
|
|
10
10
|
"timeout": 10
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-guard.sh",
|
|
15
|
+
"timeout": 10
|
|
11
16
|
}
|
|
12
17
|
]
|
|
13
18
|
},
|
|
@@ -24,6 +29,11 @@
|
|
|
24
29
|
{
|
|
25
30
|
"matcher": "Write|Edit",
|
|
26
31
|
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-write-guard.sh",
|
|
35
|
+
"timeout": 10
|
|
36
|
+
},
|
|
27
37
|
{
|
|
28
38
|
"type": "command",
|
|
29
39
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scope-guard.sh",
|
|
@@ -87,6 +97,11 @@
|
|
|
87
97
|
"type": "command",
|
|
88
98
|
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit.sh stop",
|
|
89
99
|
"timeout": 5
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop-worktree-check.sh",
|
|
104
|
+
"timeout": 10
|
|
90
105
|
}
|
|
91
106
|
]
|
|
92
107
|
}
|
|
@@ -35,5 +35,5 @@ export function verifyGitignore(projectRoot: string): Promise<boolean>;
|
|
|
35
35
|
* - Logs (caws.log, debug logs)
|
|
36
36
|
* - Local overrides (caws.local.*)
|
|
37
37
|
*/
|
|
38
|
-
export const CAWS_GITIGNORE_ENTRIES: "\n# CAWS Local Runtime Data (developer-specific, should not be tracked)\n# ====================================================================\n# Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration\n# Only local agent tracking, generated tools, and temporary files are ignored\n\n# Agent runtime tracking (local to each developer)\n.agent/\n\n# CAWS tools (now in .caws/tools/)\n.caws/tools/\n# Legacy location (for backward compatibility)\napps/tools/caws/\n\n# Temporary CAWS files\n**/*.caws.tmp\n**/*.working-spec.bak\n.caws/*.tmp\n.caws/*.bak\n\n# CAWS logs (local debugging)\ncaws-debug.log*\n**/caws.log\n.caws/*.log\n\n# Local development overrides (developer-specific)\ncaws.local.*\n.caws/local.*\n\n# CAWS Worktrees (local, should not be tracked)\n.caws/worktrees/\n.caws/worktrees.json\n";
|
|
38
|
+
export const CAWS_GITIGNORE_ENTRIES: "\n# CAWS Local Runtime Data (developer-specific, should not be tracked)\n# ====================================================================\n# Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration\n# Only local agent tracking, generated tools, and temporary files are ignored\n\n# Agent runtime tracking (local to each developer)\n.agent/\n\n# CAWS tools (now in .caws/tools/)\n.caws/tools/\n# Legacy location (for backward compatibility)\napps/tools/caws/\n\n# Temporary CAWS files\n**/*.caws.tmp\n**/*.working-spec.bak\n.caws/*.tmp\n.caws/*.bak\n\n# CAWS logs (local debugging)\ncaws-debug.log*\n**/caws.log\n.caws/*.log\n\n# Local development overrides (developer-specific)\ncaws.local.*\n.caws/local.*\n\n# CAWS Worktrees (local, should not be tracked)\n.caws/worktrees/\n.caws/worktrees.json\n\n# Session transcripts (generated by session-log hook)\ntmp/\n";
|
|
39
39
|
//# sourceMappingURL=gitignore-updater.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitignore-updater.d.ts","sourceRoot":"","sources":["../../src/utils/gitignore-updater.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gitignore-updater.d.ts","sourceRoot":"","sources":["../../src/utils/gitignore-updater.js"],"names":[],"mappings":"AAoEA;;;;;;GAMG;AACH,6CALW,MAAM,YAEd;IAAyB,KAAK,EAAtB,OAAO;CACf,GAAU,OAAO,CAAC,OAAO,CAAC,CA2D5B;AAED;;;;GAIG;AACH,6CAHW,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAW5B;AA1ID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,06BAmCE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export namespace IDE_REGISTRY {
|
|
2
|
+
namespace cursor {
|
|
3
|
+
let id: string;
|
|
4
|
+
let name: string;
|
|
5
|
+
let description: string;
|
|
6
|
+
let envVars: string[];
|
|
7
|
+
}
|
|
8
|
+
namespace claude {
|
|
9
|
+
let id_1: string;
|
|
10
|
+
export { id_1 as id };
|
|
11
|
+
let name_1: string;
|
|
12
|
+
export { name_1 as name };
|
|
13
|
+
let description_1: string;
|
|
14
|
+
export { description_1 as description };
|
|
15
|
+
let envVars_1: string[];
|
|
16
|
+
export { envVars_1 as envVars };
|
|
17
|
+
}
|
|
18
|
+
namespace vscode {
|
|
19
|
+
let id_2: string;
|
|
20
|
+
export { id_2 as id };
|
|
21
|
+
let name_2: string;
|
|
22
|
+
export { name_2 as name };
|
|
23
|
+
let description_2: string;
|
|
24
|
+
export { description_2 as description };
|
|
25
|
+
let envVars_2: string[];
|
|
26
|
+
export { envVars_2 as envVars };
|
|
27
|
+
}
|
|
28
|
+
namespace intellij {
|
|
29
|
+
let id_3: string;
|
|
30
|
+
export { id_3 as id };
|
|
31
|
+
let name_3: string;
|
|
32
|
+
export { name_3 as name };
|
|
33
|
+
let description_3: string;
|
|
34
|
+
export { description_3 as description };
|
|
35
|
+
let envVars_3: string[];
|
|
36
|
+
export { envVars_3 as envVars };
|
|
37
|
+
}
|
|
38
|
+
namespace windsurf {
|
|
39
|
+
let id_4: string;
|
|
40
|
+
export { id_4 as id };
|
|
41
|
+
let name_4: string;
|
|
42
|
+
export { name_4 as name };
|
|
43
|
+
let description_4: string;
|
|
44
|
+
export { description_4 as description };
|
|
45
|
+
let envVars_4: string[];
|
|
46
|
+
export { envVars_4 as envVars };
|
|
47
|
+
}
|
|
48
|
+
namespace copilot {
|
|
49
|
+
let id_5: string;
|
|
50
|
+
export { id_5 as id };
|
|
51
|
+
let name_5: string;
|
|
52
|
+
export { name_5 as name };
|
|
53
|
+
let description_5: string;
|
|
54
|
+
export { description_5 as description };
|
|
55
|
+
let envVars_5: any[];
|
|
56
|
+
export { envVars_5 as envVars };
|
|
57
|
+
}
|
|
58
|
+
namespace junie {
|
|
59
|
+
let id_6: string;
|
|
60
|
+
export { id_6 as id };
|
|
61
|
+
let name_6: string;
|
|
62
|
+
export { name_6 as name };
|
|
63
|
+
let description_6: string;
|
|
64
|
+
export { description_6 as description };
|
|
65
|
+
let envVars_6: any[];
|
|
66
|
+
export { envVars_6 as envVars };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export const ALL_IDE_IDS: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Detect currently active IDEs from environment variables.
|
|
72
|
+
* @returns {string[]} Array of detected IDE identifiers
|
|
73
|
+
*/
|
|
74
|
+
export function detectActiveIDEs(): string[];
|
|
75
|
+
/**
|
|
76
|
+
* Get recommended IDE set based on detection and natural pairings.
|
|
77
|
+
* - Cursor detected -> also recommend Claude Code
|
|
78
|
+
* - VS Code detected -> also recommend Copilot
|
|
79
|
+
* - Nothing detected -> default to cursor + claude (AI-first set)
|
|
80
|
+
* @returns {string[]} Array of recommended IDE identifiers
|
|
81
|
+
*/
|
|
82
|
+
export function getRecommendedIDEs(): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Parse an IDE selection from a CLI flag value or prompt answer.
|
|
85
|
+
* @param {string|string[]} input - Comma-separated string or array of IDE ids
|
|
86
|
+
* @returns {string[]} Normalized, validated array of IDE identifiers
|
|
87
|
+
*/
|
|
88
|
+
export function parseIDESelection(input: string | string[]): string[];
|
|
89
|
+
//# sourceMappingURL=ide-detection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ide-detection.d.ts","sourceRoot":"","sources":["../../src/utils/ide-detection.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,mCAA8C;AAE9C;;;GAGG;AACH,oCAFa,MAAM,EAAE,CAUpB;AAED;;;;;;GAMG;AACH,sCAFa,MAAM,EAAE,CAcpB;AAED;;;;GAIG;AACH,yCAHW,MAAM,GAAC,MAAM,EAAE,GACb,MAAM,EAAE,CA0BpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,
|
|
1
|
+
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AA6DA;;;;;GAKG;AACH,mEA8IC;AAED;;;;;GAKG;AACH,kFAgdC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
|
|
@@ -112,6 +112,22 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// Validate status field if present
|
|
116
|
+
if (spec.status) {
|
|
117
|
+
const { SPEC_STATUSES } = require('../constants/spec-types');
|
|
118
|
+
if (!SPEC_STATUSES[spec.status]) {
|
|
119
|
+
return {
|
|
120
|
+
valid: false,
|
|
121
|
+
errors: [
|
|
122
|
+
{
|
|
123
|
+
instancePath: '/status',
|
|
124
|
+
message: `Invalid status '${spec.status}'. Valid values: ${Object.keys(SPEC_STATUSES).join(', ')}`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
// Validate experimental mode
|
|
116
132
|
if (spec.experimental_mode) {
|
|
117
133
|
if (typeof spec.experimental_mode !== 'object') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,
|
|
1
|
+
{"version":3,"file":"worktree-manager.d.ts","sourceRoot":"","sources":["../../src/worktree/worktree-manager.js"],"names":[],"mappings":"AA+DA;;;;;;;;GAQG;AACH,qCAPW,MAAM,YAEd;IAAyB,KAAK,GAAtB,MAAM;IACW,UAAU,GAA3B,MAAM;IACW,MAAM,GAAvB,MAAM;CACd,OAmJF;AAED;;;GAGG;AACH,uCAqCC;AAED;;;;;;GAMG;AACH,sCALW,MAAM,YAEd;IAA0B,YAAY,GAA9B,OAAO;IACW,KAAK,GAAvB,OAAO;CACjB,QA0DA;AAED;;;;;GAKG;AACH,yCAHG;IAAyB,UAAU,GAA3B,MAAM;CACd,SAiDF;AA1VD;;;;GAIG;AACH,mCAHW,MAAM,OAahB;AAnCD;;;GAGG;AACH,+BAFa,MAAM,CAMlB;AAZD,4BAAsB,iBAAiB,CAAC;AACxC,4BAAsB,sBAAsB,CAAC;AAC7C,4BAAsB,OAAO,CAAC"}
|
|
@@ -206,6 +206,7 @@ function createWorktree(name, options = {}) {
|
|
|
206
206
|
baseBranch: base,
|
|
207
207
|
scope: scope || null,
|
|
208
208
|
specId: specId || null,
|
|
209
|
+
owner: options.owner || process.env.CLAUDE_SESSION_ID || null,
|
|
209
210
|
createdAt: new Date().toISOString(),
|
|
210
211
|
status: 'active',
|
|
211
212
|
};
|
|
@@ -276,27 +277,32 @@ function destroyWorktree(name, options = {}) {
|
|
|
276
277
|
throw new Error(`Worktree '${name}' not found in registry`);
|
|
277
278
|
}
|
|
278
279
|
|
|
279
|
-
// Remove git worktree
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (
|
|
280
|
+
// Remove git worktree — handle already-deleted directories gracefully
|
|
281
|
+
const dirExists = fs.existsSync(entry.path);
|
|
282
|
+
if (dirExists) {
|
|
283
|
+
try {
|
|
284
|
+
const args = ['worktree', 'remove'];
|
|
285
|
+
if (force) args.push('--force');
|
|
286
|
+
args.push(entry.path);
|
|
287
|
+
execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (force) {
|
|
290
|
+
// Force cleanup: remove directory manually
|
|
289
291
|
fs.removeSync(entry.path);
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
|
|
290
294
|
}
|
|
291
|
-
// Prune git worktree list
|
|
292
|
-
try {
|
|
293
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
294
|
-
} catch {
|
|
295
|
-
// Non-fatal
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
|
|
299
295
|
}
|
|
296
|
+
} else {
|
|
297
|
+
// Directory already gone — just clean up git's tracking
|
|
298
|
+
console.log(` Worktree directory already removed, cleaning up registry`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Always prune git's worktree list to stay in sync
|
|
302
|
+
try {
|
|
303
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
304
|
+
} catch {
|
|
305
|
+
// Non-fatal
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
// Optionally delete branch
|
|
@@ -337,15 +343,19 @@ function pruneWorktrees(options = {}) {
|
|
|
337
343
|
for (const [name, entry] of Object.entries(registry.worktrees)) {
|
|
338
344
|
const created = new Date(entry.createdAt);
|
|
339
345
|
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
|
346
|
+
const dirExists = fs.existsSync(entry.path);
|
|
340
347
|
|
|
341
348
|
const shouldPrune =
|
|
349
|
+
// Always prune destroyed entries
|
|
342
350
|
entry.status === 'destroyed' ||
|
|
343
|
-
|
|
344
|
-
(
|
|
351
|
+
// Prune active entries whose directory is gone (filesystem-registry desync)
|
|
352
|
+
(entry.status === 'active' && !dirExists) ||
|
|
353
|
+
// Prune old missing entries
|
|
354
|
+
(!dirExists && ageDays > maxAgeDays);
|
|
345
355
|
|
|
346
356
|
if (shouldPrune) {
|
|
347
357
|
// Clean up filesystem if still exists
|
|
348
|
-
if (
|
|
358
|
+
if (dirExists) {
|
|
349
359
|
try {
|
|
350
360
|
execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
|
|
351
361
|
cwd: root,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paths.design/caws-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.1.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": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"typecheck": "tsc --emitDeclarationOnly --outDir dist",
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"test": "npm run build && jest && npm run test:cleanup",
|
|
21
|
-
"test:unit": "npm run build && jest && npm run test:cleanup",
|
|
21
|
+
"test:unit": "npm run build && jest --testPathIgnorePatterns='perf-budgets|integration|e2e|mutation|axe|contract' && npm run test:cleanup",
|
|
22
22
|
"test:contract": "npm run build && jest --testPathPatterns=contract && npm run test:cleanup",
|
|
23
23
|
"test:integration": "npm run build && jest --testPathPatterns=integration && npm run test:cleanup",
|
|
24
24
|
"test:e2e:smoke": "npm run build && jest --testPathPatterns=e2e && npm run test:cleanup",
|