@paths.design/caws-cli 11.0.0 → 11.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +2 -2
- package/dist/init/harness-detect.d.ts +18 -0
- package/dist/init/harness-detect.d.ts.map +1 -0
- package/dist/init/harness-detect.js +90 -0
- package/dist/init/harness-detect.js.map +1 -0
- package/dist/init/hook-install.d.ts +53 -0
- package/dist/init/hook-install.d.ts.map +1 -0
- package/dist/init/hook-install.js +421 -0
- package/dist/init/hook-install.js.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.js +190 -0
- package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
- package/dist/init/hook-packs/register.d.ts +19 -0
- package/dist/init/hook-packs/register.d.ts.map +1 -0
- package/dist/init/hook-packs/register.js +37 -0
- package/dist/init/hook-packs/register.js.map +1 -0
- package/dist/init/hook-packs/types.d.ts +123 -0
- package/dist/init/hook-packs/types.d.ts.map +1 -0
- package/dist/init/hook-packs/types.js +29 -0
- package/dist/init/hook-packs/types.js.map +1 -0
- package/dist/shell/commands/gates.d.ts.map +1 -1
- package/dist/shell/commands/gates.js +28 -1
- package/dist/shell/commands/gates.js.map +1 -1
- package/dist/shell/commands/init.d.ts +9 -0
- package/dist/shell/commands/init.d.ts.map +1 -1
- package/dist/shell/commands/init.js +131 -27
- package/dist/shell/commands/init.js.map +1 -1
- package/dist/shell/commands/specs.d.ts +41 -0
- package/dist/shell/commands/specs.d.ts.map +1 -0
- package/dist/shell/commands/specs.js +264 -0
- package/dist/shell/commands/specs.js.map +1 -0
- package/dist/shell/commands/worktree.d.ts +38 -0
- package/dist/shell/commands/worktree.d.ts.map +1 -0
- package/dist/shell/commands/worktree.js +286 -0
- package/dist/shell/commands/worktree.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -1
- package/dist/shell/gates/disposition.js +33 -3
- package/dist/shell/gates/disposition.js.map +1 -1
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
- package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
- package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/index.js +67 -0
- package/dist/shell/gates/local-evaluators/index.js.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
- package/dist/shell/index.d.ts +4 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -1
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/register.d.ts.map +1 -1
- package/dist/shell/register.js +192 -2
- package/dist/shell/register.js.map +1 -1
- package/dist/shell/render/init-hook-pack.d.ts +16 -0
- package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
- package/dist/shell/render/init-hook-pack.js +206 -0
- package/dist/shell/render/init-hook-pack.js.map +1 -0
- package/dist/store/atomic-write.d.ts +20 -2
- package/dist/store/atomic-write.d.ts.map +1 -1
- package/dist/store/atomic-write.js +44 -2
- package/dist/store/atomic-write.js.map +1 -1
- package/dist/store/lifecycle-lock.d.ts +34 -0
- package/dist/store/lifecycle-lock.d.ts.map +1 -0
- package/dist/store/lifecycle-lock.js +168 -0
- package/dist/store/lifecycle-lock.js.map +1 -0
- package/dist/store/lifecycle-transaction.d.ts +79 -0
- package/dist/store/lifecycle-transaction.d.ts.map +1 -0
- package/dist/store/lifecycle-transaction.js +319 -0
- package/dist/store/lifecycle-transaction.js.map +1 -0
- package/dist/store/rules.d.ts +16 -0
- package/dist/store/rules.d.ts.map +1 -1
- package/dist/store/rules.js +17 -0
- package/dist/store/rules.js.map +1 -1
- package/dist/store/specs-writer.d.ts +61 -0
- package/dist/store/specs-writer.d.ts.map +1 -0
- package/dist/store/specs-writer.js +506 -0
- package/dist/store/specs-writer.js.map +1 -0
- package/dist/store/worktrees-writer.d.ts +77 -0
- package/dist/store/worktrees-writer.d.ts.map +1 -0
- package/dist/store/worktrees-writer.js +674 -0
- package/dist/store/worktrees-writer.js.map +1 -0
- package/dist/store/yaml-patch.d.ts +7 -0
- package/dist/store/yaml-patch.d.ts.map +1 -0
- package/dist/store/yaml-patch.js +250 -0
- package/dist/store/yaml-patch.js.map +1 -0
- package/package.json +7 -4
- package/templates/hook-packs/claude-code/CLAUDE.md +172 -0
- package/templates/hook-packs/claude-code/audit.sh +121 -0
- package/templates/hook-packs/claude-code/block-dangerous.sh +158 -0
- package/templates/hook-packs/claude-code/classify_command.py +1064 -0
- package/templates/hook-packs/claude-code/dispatch/post_tool_use.sh +63 -0
- package/templates/hook-packs/claude-code/dispatch/pre_tool_use.sh +50 -0
- package/templates/hook-packs/claude-code/dispatch/session_start.sh +41 -0
- package/templates/hook-packs/claude-code/dispatch/stop.sh +37 -0
- package/templates/hook-packs/claude-code/guard-strikes.sh +140 -0
- package/templates/hook-packs/claude-code/lib/parse-input.sh +127 -0
- package/templates/hook-packs/claude-code/lib/run-handlers.sh +212 -0
- package/templates/hook-packs/claude-code/reset-danger-latch.sh +21 -0
- package/templates/hook-packs/claude-code/reset-strikes.sh +243 -0
- package/templates/hook-packs/claude-code/runtime-paths.sh +80 -0
- package/templates/hook-packs/claude-code/scope-guard.sh +392 -0
- package/templates/hook-packs/claude-code/session-caws-status.sh +171 -0
- package/templates/hook-packs/claude-code/session-log.sh +180 -0
- package/templates/hook-packs/claude-code/worktree-guard.sh +240 -0
- package/templates/hook-packs/claude-code/worktree-write-guard.sh +77 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 10
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
# Session Logger for Claude Code — lean structured session capture.
|
|
9
|
+
#
|
|
10
|
+
# Canonical artifacts:
|
|
11
|
+
# session.json — session index + aggregated refs + git snapshot
|
|
12
|
+
# turn-001.json — per-turn detailed timeline
|
|
13
|
+
# handoff.json — compact continuation view for follow-on agents
|
|
14
|
+
# session.txt — human-readable summary pointing at the JSON artifacts
|
|
15
|
+
#
|
|
16
|
+
# Output: ./tmp/<session-id>/
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
# shellcheck source=lib/parse-input.sh
|
|
22
|
+
source "$SCRIPT_DIR/lib/parse-input.sh"
|
|
23
|
+
parse_hook_input
|
|
24
|
+
|
|
25
|
+
SESSION_ID="$HOOK_SESSION_ID"
|
|
26
|
+
HOOK_EVENT="${HOOK_EVENT_NAME:-unknown}"
|
|
27
|
+
CWD="${HOOK_CWD:-.}"
|
|
28
|
+
TRANSCRIPT_PATH="$HOOK_TRANSCRIPT_PATH"
|
|
29
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
30
|
+
|
|
31
|
+
LOG_DIR="${CWD}/tmp/${SESSION_ID}"
|
|
32
|
+
mkdir -p "$LOG_DIR"
|
|
33
|
+
|
|
34
|
+
META_FILE="$LOG_DIR/.meta.json"
|
|
35
|
+
RENDERER="$SCRIPT_DIR/session_log_renderer.py"
|
|
36
|
+
|
|
37
|
+
resolve_transcript() {
|
|
38
|
+
if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
39
|
+
printf '%s\n' "$TRANSCRIPT_PATH"
|
|
40
|
+
return
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
local slug candidate
|
|
44
|
+
slug=$(echo "$CWD" | sed 's|/|-|g; s|^-||')
|
|
45
|
+
|
|
46
|
+
candidate="$HOME/.claude/projects/${slug}/${SESSION_ID}.jsonl"
|
|
47
|
+
if [[ -f "$candidate" ]]; then
|
|
48
|
+
printf '%s\n' "$candidate"
|
|
49
|
+
return
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
candidate="$HOME/.claude/projects/-${slug}/${SESSION_ID}.jsonl"
|
|
53
|
+
if [[ -f "$candidate" ]]; then
|
|
54
|
+
printf '%s\n' "$candidate"
|
|
55
|
+
return
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
printf '\n'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render_session_output() {
|
|
62
|
+
local transcript="$1"
|
|
63
|
+
local branch head_sha dirty_count started_at model start_sha
|
|
64
|
+
|
|
65
|
+
if cd "$CWD" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
66
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
67
|
+
head_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
68
|
+
dirty_count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
69
|
+
else
|
|
70
|
+
branch="unknown"
|
|
71
|
+
head_sha="unknown"
|
|
72
|
+
dirty_count="0"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ -f "$META_FILE" ]]; then
|
|
76
|
+
started_at=$(jq -r '.local_time // "unknown"' "$META_FILE")
|
|
77
|
+
model=$(jq -r '.model // "unknown"' "$META_FILE")
|
|
78
|
+
start_sha=$(jq -r '.head_sha // ""' "$META_FILE")
|
|
79
|
+
else
|
|
80
|
+
started_at="(resumed session)"
|
|
81
|
+
model="unknown"
|
|
82
|
+
start_sha=""
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
python3 "$RENDERER" \
|
|
86
|
+
"$LOG_DIR" \
|
|
87
|
+
"$CWD" \
|
|
88
|
+
"$SESSION_ID" \
|
|
89
|
+
"$started_at" \
|
|
90
|
+
"$model" \
|
|
91
|
+
"$branch" \
|
|
92
|
+
"$head_sha" \
|
|
93
|
+
"$dirty_count" \
|
|
94
|
+
"$start_sha" \
|
|
95
|
+
"$transcript"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
handle_session_start() {
|
|
99
|
+
local model source branch head_sha dirty_count full_time
|
|
100
|
+
model="${HOOK_MODEL:-unknown}"
|
|
101
|
+
source="${HOOK_SOURCE:-unknown}"
|
|
102
|
+
if cd "$CWD" 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
103
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
104
|
+
head_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
105
|
+
dirty_count=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
106
|
+
else
|
|
107
|
+
branch="unknown"
|
|
108
|
+
head_sha="unknown"
|
|
109
|
+
dirty_count="0"
|
|
110
|
+
fi
|
|
111
|
+
full_time=$(date +"%Y-%m-%d %H:%M:%S %Z")
|
|
112
|
+
|
|
113
|
+
jq -cn \
|
|
114
|
+
--arg sid "$SESSION_ID" \
|
|
115
|
+
--arg ts "$TIMESTAMP" \
|
|
116
|
+
--arg lt "$full_time" \
|
|
117
|
+
--arg model "$model" \
|
|
118
|
+
--arg source "$source" \
|
|
119
|
+
--arg branch "$branch" \
|
|
120
|
+
--arg head "$head_sha" \
|
|
121
|
+
--arg dirty "$dirty_count" \
|
|
122
|
+
--arg project "$(basename "$CWD")" \
|
|
123
|
+
--arg transcript "$TRANSCRIPT_PATH" \
|
|
124
|
+
'{session_id: $sid, started_at: $ts, local_time: $lt, model: $model, source: $source, branch: $branch, head_sha: $head, dirty_files: $dirty, project: $project, transcript_path: $transcript}' \
|
|
125
|
+
> "$META_FILE"
|
|
126
|
+
|
|
127
|
+
render_session_output "$(resolve_transcript)"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
handle_stop() {
|
|
131
|
+
render_session_output "$(resolve_transcript)"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handle_pre_compact() {
|
|
135
|
+
render_session_output "$(resolve_transcript)"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
is_plan_file_path() {
|
|
139
|
+
local file_path
|
|
140
|
+
file_path="${1:-}"
|
|
141
|
+
|
|
142
|
+
[[ -n "$file_path" ]] || return 1
|
|
143
|
+
|
|
144
|
+
case "$file_path" in
|
|
145
|
+
"$HOME"/.claude/plans/*.md|*/.claude/plans/*.md)
|
|
146
|
+
return 0
|
|
147
|
+
;;
|
|
148
|
+
*)
|
|
149
|
+
return 1
|
|
150
|
+
;;
|
|
151
|
+
esac
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
handle_post_tool_use() {
|
|
155
|
+
local tool_name file_path
|
|
156
|
+
tool_name="$HOOK_TOOL_NAME"
|
|
157
|
+
file_path="${HOOK_FILE_PATH:-}"
|
|
158
|
+
case "$tool_name" in
|
|
159
|
+
Write|Edit)
|
|
160
|
+
if is_plan_file_path "$file_path"; then
|
|
161
|
+
render_session_output "$(resolve_transcript)"
|
|
162
|
+
fi
|
|
163
|
+
;;
|
|
164
|
+
ExitPlanMode)
|
|
165
|
+
render_session_output "$(resolve_transcript)"
|
|
166
|
+
;;
|
|
167
|
+
*)
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "$HOOK_EVENT" in
|
|
173
|
+
SessionStart) handle_session_start ;;
|
|
174
|
+
Stop) handle_stop ;;
|
|
175
|
+
PreCompact) handle_pre_compact ;;
|
|
176
|
+
PostToolUse) handle_post_tool_use ;;
|
|
177
|
+
*) ;;
|
|
178
|
+
esac
|
|
179
|
+
|
|
180
|
+
exit 0
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 4,6,11
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
#
|
|
9
|
+
# CAWS Worktree Safety Guard for Claude Code (v11-shape).
|
|
10
|
+
# Blocks dangerous git operations and cross-boundary file copies when
|
|
11
|
+
# parallel worktrees are active.
|
|
12
|
+
#
|
|
13
|
+
# Registry-shape compatibility:
|
|
14
|
+
# v11 direct-key: { "<name>": { "status": "active", ... } }
|
|
15
|
+
# v10 nested: { "worktrees": { "<name>": { ... } } }
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
|
+
# shellcheck source=lib/parse-input.sh
|
|
21
|
+
source "$SCRIPT_DIR/lib/parse-input.sh"
|
|
22
|
+
parse_hook_input
|
|
23
|
+
|
|
24
|
+
TOOL_NAME="$HOOK_TOOL_NAME"
|
|
25
|
+
COMMAND="$HOOK_COMMAND"
|
|
26
|
+
|
|
27
|
+
if [[ "$TOOL_NAME" != "Bash" ]] || [[ -z "$COMMAND" ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
32
|
+
|
|
33
|
+
if command -v git >/dev/null 2>&1; then
|
|
34
|
+
GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
35
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
|
|
36
|
+
CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
|
|
37
|
+
if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
|
|
38
|
+
PROJECT_DIR="$CANDIDATE"
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Block sparse checkout (runs before "only check git commands" early-exit)
|
|
44
|
+
if echo "$COMMAND" | grep -qE 'caws\s+(worktree\s+create|parallel\s+setup).*--scope'; then
|
|
45
|
+
echo "BLOCKED: --scope (sparse checkout) is not allowed." >&2
|
|
46
|
+
echo "Sparse checkout breaks cross-module imports in most projects." >&2
|
|
47
|
+
echo "Use full worktrees without --scope. Scope enforcement comes from" >&2
|
|
48
|
+
echo "CAWS feature specs and lane discipline, not from hiding files." >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if echo "$COMMAND" | grep -qE '(^|;|&&|\|)\s*git\s+sparse-checkout'; then
|
|
53
|
+
echo "BLOCKED: git sparse-checkout is not allowed in this project." >&2
|
|
54
|
+
echo "Use full worktrees without sparse-checkout." >&2
|
|
55
|
+
exit 2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Block cross-boundary file copies (worktree → main).
|
|
59
|
+
WORKTREE_BASE="$PROJECT_DIR/.caws/worktrees"
|
|
60
|
+
if [[ -d "$WORKTREE_BASE" ]]; then
|
|
61
|
+
if echo "$COMMAND" | grep -qE '\b(cp|mv)\b'; then
|
|
62
|
+
AGENT_IN_WORKTREE=false
|
|
63
|
+
if [[ -n "$HOOK_CWD" ]] && [[ "$HOOK_CWD" == "$WORKTREE_BASE"/* ]]; then
|
|
64
|
+
AGENT_IN_WORKTREE=true
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ "$AGENT_IN_WORKTREE" != "true" ]]; then
|
|
68
|
+
if echo "$COMMAND" | grep -qF ".caws/worktrees/" || echo "$COMMAND" | grep -qF "$WORKTREE_BASE"; then
|
|
69
|
+
HAS_WT_PATH=false
|
|
70
|
+
HAS_MAIN_PATH=false
|
|
71
|
+
if echo "$COMMAND" | grep -qE '\.caws/worktrees/|'"$(echo "$WORKTREE_BASE" | sed 's/[\/&]/\\&/g')"''; then
|
|
72
|
+
HAS_WT_PATH=true
|
|
73
|
+
fi
|
|
74
|
+
if echo "$COMMAND" | grep -qE "(^|\s)$PROJECT_DIR/[^.]|core/|src/|tests/|packages/" && [[ "$HAS_WT_PATH" == "true" ]]; then
|
|
75
|
+
HAS_MAIN_PATH=true
|
|
76
|
+
fi
|
|
77
|
+
if [[ "$HAS_WT_PATH" == "true" ]] && [[ "$HAS_MAIN_PATH" == "true" ]]; then
|
|
78
|
+
echo "BLOCKED: Copying files from a worktree to the main repo is forbidden." >&2
|
|
79
|
+
echo "This bypasses worktree isolation. Work entirely within your worktree." >&2
|
|
80
|
+
echo "If tests need the main repo's venv, activate it with:" >&2
|
|
81
|
+
echo " source $PROJECT_DIR/.venv/bin/activate" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Only check git commands from here on
|
|
90
|
+
if ! echo "$COMMAND" | grep -qE '(^|\s|&&|\|)git\s'; then
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Determine if worktrees are active (dual-shape aware).
|
|
95
|
+
#
|
|
96
|
+
# Helper logic embedded in the Node inline reads worktrees.json under both
|
|
97
|
+
# shapes. v11 direct-key: registry keys are worktree names themselves,
|
|
98
|
+
# each value is an entry object with at least { status }. v10 nested:
|
|
99
|
+
# registry.worktrees is the entry map.
|
|
100
|
+
WORKTREES_ACTIVE=false
|
|
101
|
+
PARALLEL_BASE=""
|
|
102
|
+
|
|
103
|
+
if [[ -f "$PROJECT_DIR/.caws/parallel.json" ]] && command -v node >/dev/null 2>&1; then
|
|
104
|
+
PARALLEL_INFO=$(node -e "
|
|
105
|
+
try {
|
|
106
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/parallel.json', 'utf8'));
|
|
107
|
+
var agents = (reg.agents || []).length;
|
|
108
|
+
console.log(agents + ':' + (reg.baseBranch || ''));
|
|
109
|
+
} catch(e) { console.log('0:'); }
|
|
110
|
+
" 2>/dev/null || echo "0:")
|
|
111
|
+
|
|
112
|
+
AGENT_COUNT=$(echo "$PARALLEL_INFO" | cut -d: -f1)
|
|
113
|
+
PARALLEL_BASE=$(echo "$PARALLEL_INFO" | cut -d: -f2)
|
|
114
|
+
|
|
115
|
+
if [[ "$AGENT_COUNT" -gt 0 ]] 2>/dev/null; then
|
|
116
|
+
WORKTREES_ACTIVE=true
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [[ "$WORKTREES_ACTIVE" != "true" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
|
|
121
|
+
ACTIVE_COUNT=$(node -e "
|
|
122
|
+
try {
|
|
123
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
124
|
+
// Dual-shape: v10 nested vs v11 direct-key.
|
|
125
|
+
function entriesOf(r) {
|
|
126
|
+
if (!r || typeof r !== 'object') return [];
|
|
127
|
+
if (r.worktrees && typeof r.worktrees === 'object') return Object.values(r.worktrees);
|
|
128
|
+
// v11 direct-key: filter to objects with a 'status' field to avoid
|
|
129
|
+
// mistaking top-level metadata (e.g., 'version') for an entry.
|
|
130
|
+
var out = [];
|
|
131
|
+
for (var k in r) {
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(r, k)) {
|
|
133
|
+
var v = r[k];
|
|
134
|
+
if (v && typeof v === 'object' && typeof v.status === 'string') out.push(v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
var entries = entriesOf(reg);
|
|
140
|
+
var active = entries.filter(function(w) { return w.status === 'active'; });
|
|
141
|
+
console.log(active.length);
|
|
142
|
+
} catch(e) { console.log('0'); }
|
|
143
|
+
" 2>/dev/null || echo "0")
|
|
144
|
+
|
|
145
|
+
if [[ "$ACTIVE_COUNT" -gt 0 ]] 2>/dev/null; then
|
|
146
|
+
WORKTREES_ACTIVE=true
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if [[ "$WORKTREES_ACTIVE" != "true" ]]; then
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# --- Block dangerous git operations when worktrees are active ---
|
|
155
|
+
|
|
156
|
+
if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*--amend'; then
|
|
157
|
+
echo "BLOCKED: git commit --amend is not allowed while worktrees are active." >&2
|
|
158
|
+
echo "Amending commits risks rewriting another agent's work." >&2
|
|
159
|
+
echo "Create a new commit instead." >&2
|
|
160
|
+
exit 2
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
if echo "$COMMAND" | grep -qE 'git\s+stash' && ! echo "$COMMAND" | grep -qE 'git\s+stash\s+list'; then
|
|
164
|
+
echo "BLOCKED: git stash is not allowed while worktrees are active." >&2
|
|
165
|
+
echo "Stash is shared across all worktrees and can capture or destroy another agent's work." >&2
|
|
166
|
+
echo "Commit your changes to your branch instead." >&2
|
|
167
|
+
exit 2
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then
|
|
171
|
+
echo "BLOCKED: git reset --hard is not allowed while worktrees are active." >&2
|
|
172
|
+
echo "This could discard work that other agents depend on." >&2
|
|
173
|
+
exit 2
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(--force|-f\s)'; then
|
|
177
|
+
echo "BLOCKED: Force push is not allowed while worktrees are active." >&2
|
|
178
|
+
echo "This could rewrite history that other agents have based work on." >&2
|
|
179
|
+
exit 2
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# --- Base branch protections ---
|
|
183
|
+
AGENT_DIR="${HOOK_CWD:-${CLAUDE_PROJECT_DIR:-.}}"
|
|
184
|
+
CURRENT_BRANCH=$(cd "$AGENT_DIR" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
185
|
+
|
|
186
|
+
BASE_BRANCH="$PARALLEL_BASE"
|
|
187
|
+
if [[ -z "$BASE_BRANCH" ]] && [[ -f "$PROJECT_DIR/.caws/worktrees.json" ]] && command -v node >/dev/null 2>&1; then
|
|
188
|
+
BASE_BRANCH=$(node -e "
|
|
189
|
+
try {
|
|
190
|
+
var reg = JSON.parse(require('fs').readFileSync('$PROJECT_DIR/.caws/worktrees.json', 'utf8'));
|
|
191
|
+
function entriesOf(r) {
|
|
192
|
+
if (!r || typeof r !== 'object') return [];
|
|
193
|
+
if (r.worktrees && typeof r.worktrees === 'object') return Object.values(r.worktrees);
|
|
194
|
+
var out = [];
|
|
195
|
+
for (var k in r) {
|
|
196
|
+
if (Object.prototype.hasOwnProperty.call(r, k)) {
|
|
197
|
+
var v = r[k];
|
|
198
|
+
if (v && typeof v === 'object' && typeof v.status === 'string') out.push(v);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
var entries = entriesOf(reg);
|
|
204
|
+
var active = entries.filter(function(w) { return w.status === 'active'; });
|
|
205
|
+
if (active.length > 0) console.log(active[0].baseBranch || '');
|
|
206
|
+
else console.log('');
|
|
207
|
+
} catch(e) { console.log(''); }
|
|
208
|
+
" 2>/dev/null || echo "")
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
if [[ -n "$BASE_BRANCH" ]] && [[ "$CURRENT_BRANCH" == "$BASE_BRANCH" ]]; then
|
|
212
|
+
if echo "$COMMAND" | grep -qE 'git\s+push'; then
|
|
213
|
+
echo "BLOCKED: Pushing from the base branch ($BASE_BRANCH) while worktrees are active." >&2
|
|
214
|
+
echo "You should be working in a worktree, not on the base branch." >&2
|
|
215
|
+
echo "Use: cd .caws/worktrees/<name>/" >&2
|
|
216
|
+
exit 2
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if echo "$COMMAND" | grep -qE 'git\s+merge\b'; then
|
|
220
|
+
echo '{
|
|
221
|
+
"hookSpecificOutput": {
|
|
222
|
+
"hookEventName": "PreToolUse",
|
|
223
|
+
"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."
|
|
224
|
+
}
|
|
225
|
+
}'
|
|
226
|
+
exit 0
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
if echo "$COMMAND" | grep -qE 'git\s+commit\b' && ! echo "$COMMAND" | grep -qE '--amend'; then
|
|
230
|
+
echo '{
|
|
231
|
+
"hookSpecificOutput": {
|
|
232
|
+
"hookEventName": "PreToolUse",
|
|
233
|
+
"additionalContext": "NOTE: committing to the base branch ('"$BASE_BRANCH"') while worktrees are active. Worktrees are preferred for isolated feature work, but logical checkpoint commits from the current checkout are allowed by Claude hooks. Avoid --amend and force-push while worktrees are active."
|
|
234
|
+
}
|
|
235
|
+
}'
|
|
236
|
+
exit 0
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS-MANAGED-HOOK
|
|
3
|
+
# hook_pack: claude-code
|
|
4
|
+
# hook_pack_version: 2
|
|
5
|
+
# caws_min_major: 11
|
|
6
|
+
# lineage_refs: 4,8,13
|
|
7
|
+
# do_not_edit_directly: update via `caws init --agent-surface claude-code`
|
|
8
|
+
#
|
|
9
|
+
# CAWS Worktree Write Guard for Claude Code (v11-shape, intentionally
|
|
10
|
+
# fail-open for v11.1).
|
|
11
|
+
#
|
|
12
|
+
# This hook fires on Write/Edit and currently allows all writes from the
|
|
13
|
+
# main checkout. Worktree-first enforcement returns when worktree lifecycle
|
|
14
|
+
# is restored in CLI-WORKTREE-001 (Slice 6). Until then, this hook serves
|
|
15
|
+
# as the managed-install seat for the worktree-write enforcement surface
|
|
16
|
+
# and asserts the always-allowed allowlist so .caws/, .claude/, docs/,
|
|
17
|
+
# scripts/, tmp/, and tests/ writes are never inadvertently blocked by a
|
|
18
|
+
# future enforcement pass that forgets the allowlist.
|
|
19
|
+
#
|
|
20
|
+
# Worktree-active enforcement (when restored) must read the worktrees
|
|
21
|
+
# registry under both shapes:
|
|
22
|
+
# v11 direct-key: { "<name>": { ... } }
|
|
23
|
+
# v10 nested: { "worktrees": { "<name>": { ... } } }
|
|
24
|
+
# and accept both specId (v10) and spec_id (v11) on entries.
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
29
|
+
# shellcheck source=lib/parse-input.sh
|
|
30
|
+
source "$SCRIPT_DIR/lib/parse-input.sh"
|
|
31
|
+
parse_hook_input
|
|
32
|
+
|
|
33
|
+
TOOL_NAME="$HOOK_TOOL_NAME"
|
|
34
|
+
FILE_PATH="$HOOK_FILE_PATH"
|
|
35
|
+
|
|
36
|
+
case "$TOOL_NAME" in
|
|
37
|
+
Write|Edit) ;;
|
|
38
|
+
*) exit 0 ;;
|
|
39
|
+
esac
|
|
40
|
+
|
|
41
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
42
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd || printf '%s\n' "$PROJECT_DIR")"
|
|
43
|
+
|
|
44
|
+
if command -v git >/dev/null 2>&1; then
|
|
45
|
+
GIT_COMMON_DIR=$(cd "$PROJECT_DIR" && git rev-parse --git-common-dir 2>/dev/null || echo "")
|
|
46
|
+
if [[ -n "$GIT_COMMON_DIR" ]] && [[ "$GIT_COMMON_DIR" != ".git" ]]; then
|
|
47
|
+
CANDIDATE=$(cd "$PROJECT_DIR" && cd "$GIT_COMMON_DIR/.." 2>/dev/null && pwd || echo "")
|
|
48
|
+
if [[ -n "$CANDIDATE" ]] && [[ -d "$CANDIDATE/.caws" ]]; then
|
|
49
|
+
PROJECT_DIR="$CANDIDATE"
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Always-allowed paths bypass any future enforcement.
|
|
55
|
+
# User-global Claude state lives outside the repo; .caws/, .claude/, docs/,
|
|
56
|
+
# scripts/, tmp/, .archive/, and .githooks/ are coordination/governance
|
|
57
|
+
# surfaces, not application code.
|
|
58
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
59
|
+
case "$FILE_PATH" in
|
|
60
|
+
"${HOME:-}"/.claude/*) exit 0 ;;
|
|
61
|
+
"$PROJECT_DIR"/.caws/*|.caws/*) exit 0 ;;
|
|
62
|
+
"$PROJECT_DIR"/.claude/*|.claude/*) exit 0 ;;
|
|
63
|
+
"$PROJECT_DIR"/.gitignore|.gitignore) exit 0 ;;
|
|
64
|
+
"$PROJECT_DIR"/.tmp/*|.tmp/*) exit 0 ;;
|
|
65
|
+
"$PROJECT_DIR"/tmp/*|tmp/*) exit 0 ;;
|
|
66
|
+
"$PROJECT_DIR"/.archive/*|.archive/*) exit 0 ;;
|
|
67
|
+
"$PROJECT_DIR"/.githooks/*|.githooks/*) exit 0 ;;
|
|
68
|
+
"$PROJECT_DIR"/.github/*|.github/*) exit 0 ;;
|
|
69
|
+
"$PROJECT_DIR"/docs/*|docs/*) exit 0 ;;
|
|
70
|
+
esac
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Fail-open until CLI-WORKTREE-001 (Slice 6) restores worktree lifecycle.
|
|
74
|
+
# When that lands, this hook gains worktree-active enforcement using the
|
|
75
|
+
# dual-shape registry helpers that scope-guard.sh and worktree-guard.sh
|
|
76
|
+
# already use.
|
|
77
|
+
exit 0
|