@paths.design/caws-cli 11.1.0 → 11.1.2

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.
@@ -0,0 +1,21 @@
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: 17
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ # Active checkout wrapper for the shipped CAWS danger-latch reset tool.
9
+
10
+ set -euo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
14
+ TEMPLATE_RESET="$PROJECT_DIR/packages/caws-cli/templates/.claude/hooks/reset-danger-latch.sh"
15
+
16
+ if [[ ! -x "$TEMPLATE_RESET" ]]; then
17
+ echo "reset-danger-latch.sh template is unavailable: $TEMPLATE_RESET" >&2
18
+ exit 2
19
+ fi
20
+
21
+ exec "$TEMPLATE_RESET" "$@"
@@ -0,0 +1,243 @@
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: 17
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ # reset-strikes.sh — manual reset for CAWS/Claude guard strike counters.
9
+ #
10
+ # Strikes are per-(session, guard) counters that accumulate when an agent edits
11
+ # files outside its declared CAWS scope (see guard-strikes.sh). They never
12
+ # auto-decrement, so once an agent's scope.in is legitimately corrected, strikes
13
+ # from before the correction can permanently corner the session at strike 3+
14
+ # (hard block). This tool is the user-in-the-loop escape hatch: review state,
15
+ # decide strikes are stale, reset them. Every reset is logged to
16
+ # .claude/logs/strike-resets.log for audit.
17
+ #
18
+ # --- FUTURE: auto-reset (not implemented) --------------------------------
19
+ # The mechanical version would live in scope-guard.sh, BEFORE guard_record_strike,
20
+ # with the strict predicate "file-now-matches-scope" (not just "spec-was-touched"):
21
+ # 1. Load the active spec's scope.in.
22
+ # 2. Evaluate the glob against the current REL_PATH using the same globToRegExp
23
+ # logic as worktree-write-guard.sh.
24
+ # 3. If it matches AND the spec's mtime > strike-file mtime, zero out the
25
+ # scope_guard counter for this session before recording a new strike.
26
+ # Loophole to avoid: resetting on bare spec edits would let an agent drift, edit
27
+ # the spec to silence warnings, then drift further. The match-predicate anchors
28
+ # resets to actual scope correctness.
29
+ # Only layer this on if strike-resets.log shows daily/repeated manual use.
30
+ # -------------------------------------------------------------------------
31
+ #
32
+ # @author reset-strikes (Sterling / CAWS)
33
+
34
+ set -euo pipefail
35
+
36
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
38
+ LOG_FILE="$PROJECT_DIR/.claude/logs/strike-resets.log"
39
+
40
+ MODE="list"
41
+ SESSION=""
42
+ GUARD=""
43
+ WORKTREE=""
44
+ OLDER_THAN_DAYS=7
45
+ DRY_RUN=0
46
+ CONFIRM=0
47
+
48
+ usage() {
49
+ cat <<EOF
50
+ reset-strikes.sh — inspect and reset CAWS guard strike counters.
51
+
52
+ Default (no args): list current strike state across all sessions/worktrees.
53
+
54
+ Modes (mutually exclusive):
55
+ --session <uuid> Reset strikes for one session
56
+ --worktree <name> Reset strikes stored inside one worktree's tmp/
57
+ --current Reset the most-recently-modified strike file
58
+ --all Reset every strike file (requires --confirm)
59
+ --stale Delete strike files older than N days (see --older-than)
60
+
61
+ Modifiers:
62
+ --guard <name> Restrict reset to one guard key (e.g. scope_guard)
63
+ Used with --session / --worktree / --current.
64
+ --older-than <days> For --stale. Default: 7.
65
+ --dry-run Print what would change; don't modify files.
66
+ --confirm Required for --all.
67
+ -h, --help Show this help.
68
+
69
+ Examples:
70
+ $(basename "$0") # list state only
71
+ $(basename "$0") --current # reset most-recent session
72
+ $(basename "$0") --session abc --guard scope_guard
73
+ $(basename "$0") --stale --older-than 14 --dry-run
74
+ $(basename "$0") --all --confirm
75
+
76
+ Log of resets: $LOG_FILE
77
+ EOF
78
+ }
79
+
80
+ while [[ $# -gt 0 ]]; do
81
+ case "$1" in
82
+ --session) MODE="session"; SESSION="$2"; shift 2 ;;
83
+ --worktree) MODE="worktree"; WORKTREE="$2"; shift 2 ;;
84
+ --current) MODE="current"; shift ;;
85
+ --all) MODE="all"; shift ;;
86
+ --stale) MODE="stale"; shift ;;
87
+ --guard) GUARD="$2"; shift 2 ;;
88
+ --older-than) OLDER_THAN_DAYS="$2"; shift 2 ;;
89
+ --dry-run) DRY_RUN=1; shift ;;
90
+ --confirm) CONFIRM=1; shift ;;
91
+ -h|--help) usage; exit 0 ;;
92
+ *) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;;
93
+ esac
94
+ done
95
+
96
+ mkdir -p "$(dirname "$LOG_FILE")"
97
+
98
+ # Collect strike files from both the main-repo log dir and every worktree's tmp/.
99
+ collect_strike_files() {
100
+ {
101
+ find "$PROJECT_DIR/.claude/logs" -maxdepth 1 -name 'guard-strikes-*.json' 2>/dev/null || true
102
+ find "$PROJECT_DIR/.caws/worktrees" -maxdepth 3 -name 'guard-strikes-*.json' 2>/dev/null || true
103
+ } | sort -u
104
+ }
105
+
106
+ # macOS and Linux disagree on stat flags. Try BSD first, fall back to GNU.
107
+ file_mtime() {
108
+ stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$1" 2>/dev/null \
109
+ || stat -c '%y' "$1" 2>/dev/null | cut -d'.' -f1 \
110
+ || echo "unknown"
111
+ }
112
+
113
+ describe_file() {
114
+ local f="$1"
115
+ local mtime sid content
116
+ mtime=$(file_mtime "$f")
117
+ sid=$(basename "$f" | sed 's/^guard-strikes-//; s/\.json$//')
118
+ content=$(cat "$f" 2>/dev/null || echo '{}')
119
+ printf ' %s session=%s\n strikes=%s\n path=%s\n\n' \
120
+ "$mtime" "$sid" "$content" "$f"
121
+ }
122
+
123
+ log_reset() {
124
+ local action="$1" target="$2" before="$3"
125
+ # Flatten the JSON payload to one line so the log stays greppable.
126
+ local before_flat
127
+ before_flat=$(printf '%s' "$before" | jq -c . 2>/dev/null || printf '%s' "$before" | tr -d '\n')
128
+ printf '%s action=%s guard=%s dry_run=%s before=%s target=%s\n' \
129
+ "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$action" "${GUARD:-*}" "$DRY_RUN" "$before_flat" "$target" \
130
+ >> "$LOG_FILE"
131
+ }
132
+
133
+ reset_file() {
134
+ local f="$1"
135
+ [[ -z "$f" ]] && return 0
136
+ [[ ! -f "$f" ]] && { echo "skip (not a file): $f" >&2; return 0; }
137
+
138
+ local before
139
+ before=$(cat "$f" 2>/dev/null || echo '{}')
140
+
141
+ if [[ -z "$GUARD" ]]; then
142
+ if [[ "$DRY_RUN" == 1 ]]; then
143
+ echo "[dry-run] would delete $f (was: $before)"
144
+ log_reset "dry-run-delete" "$f" "$before"
145
+ else
146
+ rm -f "$f"
147
+ log_reset "delete" "$f" "$before"
148
+ echo "deleted: $f"
149
+ fi
150
+ else
151
+ if [[ "$DRY_RUN" == 1 ]]; then
152
+ echo "[dry-run] would clear guard '$GUARD' in $f (was: $before)"
153
+ log_reset "dry-run-clear" "$f" "$before"
154
+ else
155
+ jq --arg g "$GUARD" 'del(.[$g])' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
156
+ # If no guard keys remain, remove the file entirely.
157
+ if [[ "$(jq 'length' "$f" 2>/dev/null || echo 1)" == "0" ]]; then
158
+ rm -f "$f"
159
+ echo "cleared guard '$GUARD' and removed empty file: $f"
160
+ else
161
+ echo "cleared guard '$GUARD' in: $f"
162
+ fi
163
+ log_reset "clear-guard" "$f" "$before"
164
+ fi
165
+ fi
166
+ }
167
+
168
+ case "$MODE" in
169
+ list)
170
+ files=$(collect_strike_files)
171
+ if [[ -z "$files" ]]; then
172
+ echo "No strike files found."
173
+ exit 0
174
+ fi
175
+ echo "Current strike state:"
176
+ echo
177
+ while IFS= read -r f; do
178
+ [[ -z "$f" ]] && continue
179
+ describe_file "$f"
180
+ done <<< "$files"
181
+ echo "To reset: use --current, --session <uuid>, --worktree <name>, --stale, or --all --confirm."
182
+ ;;
183
+
184
+ current)
185
+ files=$(collect_strike_files)
186
+ # Most-recently-modified first. Handles spaces in paths defensively.
187
+ target=""
188
+ latest=0
189
+ while IFS= read -r f; do
190
+ [[ -z "$f" ]] && continue
191
+ mt=$(stat -f '%m' "$f" 2>/dev/null || stat -c '%Y' "$f" 2>/dev/null || echo 0)
192
+ if (( mt > latest )); then
193
+ latest=$mt
194
+ target="$f"
195
+ fi
196
+ done <<< "$files"
197
+ [[ -z "$target" ]] && { echo "No strike files found." >&2; exit 1; }
198
+ echo "Most-recently-modified: $target"
199
+ reset_file "$target"
200
+ ;;
201
+
202
+ session)
203
+ [[ -z "$SESSION" ]] && { echo "--session requires a uuid" >&2; exit 1; }
204
+ matches=$(collect_strike_files | grep "guard-strikes-${SESSION}\.json$" || true)
205
+ [[ -z "$matches" ]] && { echo "No strike file found for session: $SESSION" >&2; exit 1; }
206
+ while IFS= read -r f; do reset_file "$f"; done <<< "$matches"
207
+ ;;
208
+
209
+ worktree)
210
+ [[ -z "$WORKTREE" ]] && { echo "--worktree requires a name" >&2; exit 1; }
211
+ wt_dir="$PROJECT_DIR/.caws/worktrees/$WORKTREE/tmp"
212
+ if [[ ! -d "$wt_dir" ]]; then
213
+ echo "Worktree tmp dir not found: $wt_dir" >&2
214
+ exit 1
215
+ fi
216
+ matches=$(find "$wt_dir" -maxdepth 1 -name 'guard-strikes-*.json' 2>/dev/null || true)
217
+ [[ -z "$matches" ]] && { echo "No strike files in worktree: $WORKTREE" >&2; exit 1; }
218
+ while IFS= read -r f; do reset_file "$f"; done <<< "$matches"
219
+ ;;
220
+
221
+ all)
222
+ if [[ "$CONFIRM" != 1 ]]; then
223
+ echo "--all requires --confirm (safety interlock)." >&2
224
+ exit 1
225
+ fi
226
+ files=$(collect_strike_files)
227
+ [[ -z "$files" ]] && { echo "No strike files found."; exit 0; }
228
+ while IFS= read -r f; do reset_file "$f"; done <<< "$files"
229
+ ;;
230
+
231
+ stale)
232
+ files=$(find \
233
+ "$PROJECT_DIR/.claude/logs" \
234
+ "$PROJECT_DIR/.caws/worktrees" \
235
+ -name 'guard-strikes-*.json' -mtime "+$OLDER_THAN_DAYS" 2>/dev/null || true)
236
+ if [[ -z "$files" ]]; then
237
+ echo "No strike files older than $OLDER_THAN_DAYS days."
238
+ exit 0
239
+ fi
240
+ echo "Pruning strike files older than $OLDER_THAN_DAYS days:"
241
+ while IFS= read -r f; do reset_file "$f"; done <<< "$files"
242
+ ;;
243
+ esac
@@ -0,0 +1,80 @@
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: 8,16
7
+ # do_not_edit_directly: update via `caws init --agent-surface claude-code`
8
+ # Shared runtime bootstrap for Claude hook scripts.
9
+ # Ensures common developer-installed binaries remain available when hooks run
10
+ # under a reduced PATH that does not load interactive shell init.
11
+ #
12
+ # If you are reading this because a hook failed, do not patch PATH handling here
13
+ # as an unblock shortcut. Fix the real issue in the worktree/spec setup, or ask
14
+ # the user if the hook runtime itself truly needs to change.
15
+
16
+ ensure_hook_runtime_path() {
17
+ if command -v node >/dev/null 2>&1; then
18
+ return 0
19
+ fi
20
+
21
+ local latest_node_bin=""
22
+
23
+ if [[ -d "$HOME/.nvm/versions/node" ]]; then
24
+ latest_node_bin=$(
25
+ find "$HOME/.nvm/versions/node" -maxdepth 4 -type f -name node 2>/dev/null \
26
+ | sed 's#/node$##' \
27
+ | sort -V \
28
+ | tail -n 1
29
+ )
30
+ fi
31
+
32
+ if [[ -n "$latest_node_bin" ]] && [[ -d "$latest_node_bin" ]]; then
33
+ PATH="$latest_node_bin:$PATH"
34
+ fi
35
+
36
+ for candidate in /opt/homebrew/bin /usr/local/bin /usr/bin /bin; do
37
+ if [[ -d "$candidate" ]] && [[ ":$PATH:" != *":$candidate:"* ]]; then
38
+ PATH="$candidate:$PATH"
39
+ fi
40
+ done
41
+
42
+ export PATH
43
+ }
44
+
45
+ read_hook_input_json() {
46
+ python3 -c '
47
+ import json
48
+ import sys
49
+
50
+ raw = sys.stdin.buffer.read()
51
+ if not raw:
52
+ sys.stdout.write("{}")
53
+ raise SystemExit(0)
54
+
55
+ def strip_disallowed_controls(text: str) -> str:
56
+ return "".join(
57
+ ch
58
+ for ch in text
59
+ if ch in ("\t", "\n", "\r") or ord(ch) >= 0x20
60
+ )
61
+
62
+ text = raw.decode("utf-8", "surrogateescape")
63
+ sanitized = strip_disallowed_controls(text.replace("\x00", ""))
64
+
65
+ for candidate in (text, sanitized):
66
+ try:
67
+ payload = json.loads(candidate, strict=False)
68
+ except Exception:
69
+ continue
70
+ sys.stdout.write(json.dumps(payload))
71
+ raise SystemExit(0)
72
+
73
+ # Never echo malformed raw input back to jq callers. Hook scripts should
74
+ # fail open on unreadable input rather than turning parse noise into
75
+ # blocking PreToolUse/PostToolUse errors.
76
+ sys.stdout.write("{}")
77
+ ' 2>/dev/null
78
+ }
79
+
80
+ ensure_hook_runtime_path