uv-suite 0.26.5 → 0.28.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.
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: read or change auto-checkpoint settings.
3
+ # Used by the /auto-checkpoint slash command.
4
+ #
5
+ # Usage:
6
+ # auto-checkpoint-helper.sh status
7
+ # auto-checkpoint-helper.sh on
8
+ # auto-checkpoint-helper.sh off
9
+ # auto-checkpoint-helper.sh <minutes> # set interval
10
+
11
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
12
+ mkdir -p "$STATE_DIR"
13
+ STATE_FILE="$STATE_DIR/auto-checkpoint.json"
14
+
15
+ ensure_state() {
16
+ [ -f "$STATE_FILE" ] && return
17
+ cat > "$STATE_FILE" <<'EOF'
18
+ {
19
+ "mode": "on",
20
+ "interval_minutes": 10
21
+ }
22
+ EOF
23
+ }
24
+
25
+ get_field() {
26
+ ensure_state
27
+ STATE_PATH="$STATE_FILE" KEY="$1" python3 -c '
28
+ import json, os
29
+ d = json.load(open(os.environ["STATE_PATH"]))
30
+ print(d.get(os.environ["KEY"], ""))
31
+ '
32
+ }
33
+
34
+ set_field() {
35
+ ensure_state
36
+ STATE_PATH="$STATE_FILE" KEY="$1" VAL="$2" python3 -c '
37
+ import json, os
38
+ p = os.environ["STATE_PATH"]
39
+ d = json.load(open(p))
40
+ key = os.environ["KEY"]
41
+ val = os.environ["VAL"]
42
+ if key == "interval_minutes":
43
+ val = int(val)
44
+ d[key] = val
45
+ json.dump(d, open(p, "w"), indent=2)
46
+ '
47
+ }
48
+
49
+ ARG=$(printf '%s' "$1" | tr -d '[:space:]')
50
+
51
+ case "$ARG" in
52
+ on)
53
+ set_field mode on
54
+ echo "Auto-checkpoint: ON (every $(get_field interval_minutes) min, mechanical + semantic)"
55
+ ;;
56
+ off)
57
+ set_field mode off
58
+ echo "Auto-checkpoint: OFF"
59
+ ;;
60
+ ""|status)
61
+ ensure_state
62
+ echo "Auto-checkpoint: $(get_field mode) (every $(get_field interval_minutes) min)"
63
+ ;;
64
+ *)
65
+ if printf '%s' "$ARG" | grep -qE '^[0-9]+$' && [ "$ARG" -ge 1 ] && [ "$ARG" -le 1440 ]; then
66
+ set_field interval_minutes "$ARG"
67
+ echo "Auto-checkpoint interval: $ARG min (mode: $(get_field mode))"
68
+ else
69
+ echo "Usage: /auto-checkpoint [on | off | <minutes 1-1440> | status]"
70
+ exit 1
71
+ fi
72
+ ;;
73
+ esac
@@ -0,0 +1,184 @@
1
+ #!/bin/bash
2
+ # UV Suite Hook: Tier A auto-checkpoint — mechanical state snapshot.
3
+ # Event: PostToolUse
4
+ #
5
+ # Logs every tool call into a per-session activity log. When `interval_minutes`
6
+ # have passed since the last mechanical checkpoint AND there has been activity
7
+ # in the interval, writes a deterministic snapshot to
8
+ # uv-out/checkpoints/<sid>/auto-<ts>-mechanical.md and forwards an
9
+ # AutoCheckpoint event to the watchtower.
10
+ #
11
+ # Tier B (semantic, claude -p) runs separately from the watchtower's timer.
12
+
13
+ INPUT=$(cat 2>/dev/null || true)
14
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
15
+ STATE_DIR="$PROJECT_DIR/.uv-suite-state"
16
+ SESSIONS_DIR="$STATE_DIR/sessions"
17
+ mkdir -p "$SESSIONS_DIR" 2>/dev/null
18
+
19
+ STATE_FILE="$STATE_DIR/auto-checkpoint.json"
20
+
21
+ # Default state if missing
22
+ if [ ! -f "$STATE_FILE" ]; then
23
+ cat > "$STATE_FILE" <<'EOF'
24
+ {
25
+ "mode": "on",
26
+ "interval_minutes": 10
27
+ }
28
+ EOF
29
+ fi
30
+
31
+ # Read mode + interval (skip if mode != on)
32
+ read_state() {
33
+ STATE_PATH="$STATE_FILE" python3 -c '
34
+ import json, os, sys
35
+ try:
36
+ d = json.load(open(os.environ["STATE_PATH"]))
37
+ print(d.get("mode", "on"))
38
+ print(d.get("interval_minutes", 10))
39
+ except Exception:
40
+ print("on"); print("10")
41
+ ' 2>/dev/null
42
+ }
43
+
44
+ STATE_OUT=$(read_state)
45
+ MODE=$(echo "$STATE_OUT" | sed -n 1p)
46
+ INTERVAL_MIN=$(echo "$STATE_OUT" | sed -n 2p)
47
+ [ "$MODE" != "on" ] && exit 0
48
+ [ -z "$INTERVAL_MIN" ] && INTERVAL_MIN=10
49
+
50
+ # Resolve session id
51
+ SID="${UVS_SESSION_ID:-}"
52
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
53
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
54
+ fi
55
+ [ -z "$SID" ] && exit 0
56
+
57
+ ACTIVITY_LOG="$SESSIONS_DIR/$SID.activity.log"
58
+ LAST_CP_FILE="$SESSIONS_DIR/$SID.last-mechanical-checkpoint.txt"
59
+ NOW=$(date +%s)
60
+
61
+ # Append activity entry. Cap log at last 500 lines.
62
+ TOOL=""
63
+ TARGET=""
64
+ if command -v jq >/dev/null 2>&1; then
65
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
66
+ TARGET=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // .tool_input.pattern // .tool_input.url // ""' 2>/dev/null)
67
+ fi
68
+ TARGET=$(printf '%s' "$TARGET" | tr -d '\n' | cut -c1-120)
69
+ [ -n "$TOOL" ] && echo "$NOW $TOOL $TARGET" >> "$ACTIVITY_LOG"
70
+ if [ -f "$ACTIVITY_LOG" ]; then
71
+ LINES=$(wc -l < "$ACTIVITY_LOG" 2>/dev/null | tr -d ' ')
72
+ if [ -n "$LINES" ] && [ "$LINES" -gt 500 ]; then
73
+ tail -n 500 "$ACTIVITY_LOG" > "$ACTIVITY_LOG.tmp" && mv "$ACTIVITY_LOG.tmp" "$ACTIVITY_LOG"
74
+ fi
75
+ fi
76
+
77
+ # Has the interval passed since last mechanical checkpoint?
78
+ # On the very first run, seed the timestamp with NOW so we don't fire a
79
+ # checkpoint immediately — the first one fires after the interval has elapsed.
80
+ if [ ! -f "$LAST_CP_FILE" ]; then
81
+ echo "$NOW" > "$LAST_CP_FILE"
82
+ exit 0
83
+ fi
84
+ LAST_CP=$(cat "$LAST_CP_FILE" 2>/dev/null)
85
+ LAST_CP=${LAST_CP:-0}
86
+ ELAPSED=$((NOW - LAST_CP))
87
+ INTERVAL_SEC=$((INTERVAL_MIN * 60))
88
+ [ "$ELAPSED" -lt "$INTERVAL_SEC" ] && exit 0
89
+
90
+ # Activity since last checkpoint? If none, skip.
91
+ SINCE_LINES=0
92
+ if [ -f "$ACTIVITY_LOG" ]; then
93
+ SINCE_LINES=$(awk -v cutoff="$LAST_CP" '$1 > cutoff' "$ACTIVITY_LOG" 2>/dev/null | wc -l | tr -d ' ')
94
+ fi
95
+ [ "${SINCE_LINES:-0}" -lt 1 ] && exit 0
96
+
97
+ # Resolve checkpoint dir + metadata via the existing helper
98
+ CP_DIR=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$PROJECT_DIR/.claude/hooks/checkpoint-helper.sh" dir 2>/dev/null)
99
+ [ -z "$CP_DIR" ] && CP_DIR="$PROJECT_DIR/uv-out/checkpoints/$SID" && mkdir -p "$CP_DIR"
100
+
101
+ # Build the mechanical checkpoint body
102
+ TS_FILE=$(date +%Y-%m-%d-%H%M)
103
+ TS_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
104
+ CP_FILE="$CP_DIR/auto-$TS_FILE-mechanical.md"
105
+
106
+ # Frontmatter from helper, augmented with checkpoint_kind
107
+ FRONT=$(CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$PROJECT_DIR/.claude/hooks/checkpoint-helper.sh" frontmatter 2>/dev/null)
108
+ # Insert checkpoint_kind: auto-mechanical before closing ---
109
+ FRONT=$(printf '%s' "$FRONT" | awk '
110
+ /^---$/ { count++ }
111
+ count == 2 && !done { print "checkpoint_kind: auto-mechanical"; done=1 }
112
+ { print }
113
+ ')
114
+
115
+ # Activity summary from the log
116
+ ACTIVITY_SUMMARY=$(awk -v cutoff="$LAST_CP" '$1 > cutoff' "$ACTIVITY_LOG" 2>/dev/null | python3 -c '
117
+ import sys, collections, os
118
+ lines = [l.strip() for l in sys.stdin if l.strip()]
119
+ tools = collections.Counter()
120
+ files = collections.Counter()
121
+ for ln in lines:
122
+ parts = ln.split(" ", 2)
123
+ if len(parts) < 2: continue
124
+ tool = parts[1]
125
+ target = parts[2] if len(parts) > 2 else ""
126
+ tools[tool] += 1
127
+ if target and tool in ("Edit", "Write", "Read"):
128
+ files[target] += 1
129
+ print("### Tool calls")
130
+ for t, n in tools.most_common(8):
131
+ print(f"- {n}× {t}")
132
+ if files:
133
+ print("\n### Files touched")
134
+ for f, n in files.most_common(8):
135
+ print(f"- {f} ({n})")
136
+ print(f"\n_total tool calls in window: {sum(tools.values())}_")
137
+ ' 2>/dev/null)
138
+
139
+ # Git state (best-effort, may be absent in non-git dirs)
140
+ GIT_BRANCH=$(cd "$PROJECT_DIR" && git branch --show-current 2>/dev/null)
141
+ GIT_STATUS=$(cd "$PROJECT_DIR" && git status --short 2>/dev/null | head -20)
142
+ GIT_LOG=$(cd "$PROJECT_DIR" && git log --oneline -5 2>/dev/null)
143
+
144
+ {
145
+ printf '%s\n\n' "$FRONT"
146
+ printf '# Auto-checkpoint (mechanical): %s\n\n' "$TS_ISO"
147
+ printf '_window: last %s min, %s tool calls_\n\n' "$INTERVAL_MIN" "$SINCE_LINES"
148
+ printf '## Activity\n\n%s\n\n' "$ACTIVITY_SUMMARY"
149
+ if [ -n "$GIT_BRANCH" ]; then
150
+ printf '## Git\n\n'
151
+ printf '**Branch:** %s\n\n' "$GIT_BRANCH"
152
+ if [ -n "$GIT_STATUS" ]; then
153
+ printf '**Status:**\n```\n%s\n```\n\n' "$GIT_STATUS"
154
+ else
155
+ printf '**Status:** clean\n\n'
156
+ fi
157
+ if [ -n "$GIT_LOG" ]; then
158
+ printf '**Recent commits:**\n```\n%s\n```\n' "$GIT_LOG"
159
+ fi
160
+ fi
161
+ } > "$CP_FILE"
162
+
163
+ # Update latest.md so /restore picks it up
164
+ cp "$CP_FILE" "$CP_DIR/latest.md" 2>/dev/null
165
+ echo "$NOW" > "$LAST_CP_FILE"
166
+
167
+ # Send AutoCheckpoint event to watchtower with inline content
168
+ if [ -x "$PROJECT_DIR/.claude/hooks/watchtower-send.sh" ]; then
169
+ PREVIEW=$(head -c 2000 "$CP_FILE")
170
+ PAYLOAD=$(PREVIEW_PATH="$CP_FILE" PREVIEW="$PREVIEW" INTERVAL_MIN="$INTERVAL_MIN" SINCE_LINES="$SINCE_LINES" CWD_VAR="$PROJECT_DIR" python3 -c '
171
+ import json, os
172
+ print(json.dumps({
173
+ "checkpoint_kind": "auto-mechanical",
174
+ "checkpoint_path": os.environ["PREVIEW_PATH"],
175
+ "checkpoint_preview": os.environ["PREVIEW"],
176
+ "interval_minutes": int(os.environ["INTERVAL_MIN"]),
177
+ "tool_calls_in_window": int(os.environ["SINCE_LINES"]),
178
+ "cwd": os.environ["CWD_VAR"],
179
+ }))
180
+ ' 2>/dev/null)
181
+ printf '%s' "$PAYLOAD" | CLAUDE_PROJECT_DIR="$PROJECT_DIR" "$PROJECT_DIR/.claude/hooks/watchtower-send.sh" AutoCheckpoint 2>/dev/null
182
+ fi
183
+
184
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: toggle confirm-mode or change the threshold.
3
+ # Used by the /confirm slash command. Extracted into a script so the
4
+ # slash command can avoid inline ${...} expansions, which Claude Code's
5
+ # permission heuristic flags as obfuscation.
6
+ #
7
+ # Usage:
8
+ # confirm-helper.sh # show status
9
+ # confirm-helper.sh status
10
+ # confirm-helper.sh on
11
+ # confirm-helper.sh off
12
+ # confirm-helper.sh <number> # set threshold
13
+
14
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
15
+ mkdir -p "$STATE_DIR"
16
+ MODE_FILE="$STATE_DIR/confirm-mode.txt"
17
+ THRESH_FILE="$STATE_DIR/confirm-threshold.txt"
18
+
19
+ current_mode() { cat "$MODE_FILE" 2>/dev/null || echo "on"; }
20
+ current_thresh() { cat "$THRESH_FILE" 2>/dev/null || echo "50"; }
21
+
22
+ ARG=$(printf '%s' "$1" | tr -d '[:space:]')
23
+
24
+ case "$ARG" in
25
+ on)
26
+ echo "on" > "$MODE_FILE"
27
+ echo "Confirm mode: ON (threshold: $(current_thresh) words)"
28
+ ;;
29
+ off)
30
+ echo "off" > "$MODE_FILE"
31
+ echo "Confirm mode: OFF"
32
+ ;;
33
+ ""|status)
34
+ echo "Confirm mode: $(current_mode) (threshold: $(current_thresh) words)"
35
+ ;;
36
+ *)
37
+ if printf '%s' "$ARG" | grep -qE '^[0-9]+$'; then
38
+ echo "$ARG" > "$THRESH_FILE"
39
+ echo "Threshold set to $ARG words (mode: $(current_mode))"
40
+ else
41
+ echo "Usage: /confirm [on | off | <number> | status]"
42
+ exit 1
43
+ fi
44
+ ;;
45
+ esac
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # UV Suite helper: explicit session termination.
3
+ # Used by the /session-end slash command.
4
+ #
5
+ # Writes a "terminated_at" timestamp to the session metadata, fires a
6
+ # SessionEnd event to the Watchtower so the dashboard updates the status
7
+ # badge, and prints a one-line confirmation.
8
+
9
+ STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"
10
+ SESSIONS_DIR="$STATE_DIR/sessions"
11
+ mkdir -p "$SESSIONS_DIR"
12
+
13
+ SID="${UVS_SESSION_ID:-}"
14
+ if [ -z "$SID" ] && [ -f "$STATE_DIR/current-session.txt" ]; then
15
+ SID=$(cat "$STATE_DIR/current-session.txt" 2>/dev/null)
16
+ fi
17
+
18
+ if [ -z "$SID" ]; then
19
+ echo "No active UV Suite session to end."
20
+ exit 1
21
+ fi
22
+
23
+ META="$SESSIONS_DIR/$SID.json"
24
+ NOW=$(date +%s)
25
+ NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
26
+
27
+ if [ -f "$META" ]; then
28
+ META_PATH="$META" NOW_VAL="$NOW" NOW_ISO_VAL="$NOW_ISO" python3 - <<'PY'
29
+ import json, os
30
+ p = os.environ["META_PATH"]
31
+ d = json.load(open(p))
32
+ d["terminated_at"] = int(os.environ["NOW_VAL"])
33
+ d["terminated_at_iso"] = os.environ["NOW_ISO_VAL"]
34
+ d["lifecycle"] = "terminated"
35
+ json.dump(d, open(p, "w"), indent=2)
36
+ PY
37
+ fi
38
+
39
+ # Fire SessionEnd event so the dashboard flips the status badge to Terminated.
40
+ if [ -x "$CLAUDE_PROJECT_DIR/.claude/hooks/watchtower-send.sh" ]; then
41
+ PAYLOAD=$(SID_VAL="$SID" NOW_VAL="$NOW" CWD_VAL="${CLAUDE_PROJECT_DIR:-$(pwd)}" python3 -c '
42
+ import json, os
43
+ print(json.dumps({
44
+ "uvs_session_id": os.environ["SID_VAL"],
45
+ "session_id": os.environ["SID_VAL"],
46
+ "cwd": os.environ["CWD_VAL"],
47
+ "lifecycle": "terminated",
48
+ "terminated_at": int(os.environ["NOW_VAL"]),
49
+ "terminated_by": "user",
50
+ }))
51
+ ')
52
+ printf '%s' "$PAYLOAD" | "$CLAUDE_PROJECT_DIR/.claude/hooks/watchtower-send.sh" SessionEnd 2>/dev/null
53
+ fi
54
+
55
+ echo "Session ${SID:0:8} marked terminated at $NOW_ISO."
56
+ echo "Run /checkpoint first if you want a final state snapshot, then exit the terminal."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uv-suite",
3
- "version": "0.26.5",
3
+ "version": "0.28.0",
4
4
  "description": "Portable framework for AI-assisted software development. 10 agents, 9 skills, 5 hooks, 4 personas. Works with Claude Code, Cursor, and Codex.",
5
5
  "author": "Utsav Anand",
6
6
  "license": "MIT",
@@ -37,14 +37,22 @@
37
37
  "Bash(wc *)",
38
38
  "Bash(head *)",
39
39
  "Bash(tail *)",
40
- "Bash(curl *)"
40
+ "Bash(curl *)",
41
+ "Bash(chmod *)",
42
+ "Bash(rm /tmp/*)",
43
+ "Bash(echo *)",
44
+ "Bash(printf *)",
45
+ "Bash(node --check *)",
46
+ "Bash(bash -n *)"
41
47
  ],
42
48
  "deny": [
43
49
  "Bash(rm -rf /)",
44
50
  "Bash(rm -rf ~)",
45
51
  "Bash(sudo rm -rf *)",
46
52
  "Bash(git push --force * main)",
47
- "Bash(git push --force * master)"
53
+ "Bash(git push --force * master)",
54
+ "Bash(rm -rf .)",
55
+ "Bash(sudo rm *)"
48
56
  ]
49
57
  },
50
58
  "hooks": {
@@ -155,6 +163,12 @@
155
163
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
156
164
  "timeout": 2,
157
165
  "async": true
166
+ },
167
+ {
168
+ "type": "command",
169
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-checkpoint.sh",
170
+ "timeout": 5,
171
+ "async": true
158
172
  }
159
173
  ]
160
174
  },
@@ -33,7 +33,16 @@
33
33
  "Bash(npx prettier *)",
34
34
  "Bash(pytest *)",
35
35
  "Bash(go test *)",
36
- "Bash(cargo test *)"
36
+ "Bash(cargo test *)",
37
+ "Bash(chmod *)",
38
+ "Bash(mkdir *)",
39
+ "Bash(rm /tmp/*)",
40
+ "Bash(cat *)",
41
+ "Bash(ls *)",
42
+ "Bash(echo *)",
43
+ "Bash(printf *)",
44
+ "Bash(node --check *)",
45
+ "Bash(bash -n *)"
37
46
  ],
38
47
  "deny": [
39
48
  "Bash(rm -rf /)",
@@ -42,7 +51,8 @@
42
51
  "Bash(sudo rm *)",
43
52
  "Bash(git push --force * main)",
44
53
  "Bash(git push --force * master)",
45
- "Bash(git reset --hard origin/*)"
54
+ "Bash(git reset --hard origin/*)",
55
+ "Bash(sudo rm -rf *)"
46
56
  ]
47
57
  },
48
58
  "hooks": {
@@ -164,6 +174,12 @@
164
174
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
165
175
  "timeout": 2,
166
176
  "async": true
177
+ },
178
+ {
179
+ "type": "command",
180
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-checkpoint.sh",
181
+ "timeout": 5,
182
+ "async": true
167
183
  }
168
184
  ]
169
185
  },
@@ -22,15 +22,30 @@
22
22
  "Bash(find *)",
23
23
  "Bash(wc *)",
24
24
  "Bash(head *)",
25
- "Bash(tail *)"
25
+ "Bash(tail *)",
26
+ "Edit(*)",
27
+ "Bash(git *)",
28
+ "Bash(npx prettier *)",
29
+ "Bash(npx markdown-* *)",
30
+ "Bash(curl *)",
31
+ "Bash(chmod *)",
32
+ "Bash(mkdir *)",
33
+ "Bash(rm /tmp/*)",
34
+ "Bash(cat *)",
35
+ "Bash(ls *)",
36
+ "Bash(echo *)",
37
+ "Bash(printf *)",
38
+ "Bash(node --check *)",
39
+ "Bash(bash -n *)"
26
40
  ],
27
41
  "deny": [
28
- "Edit(*)",
29
- "Bash(git commit *)",
30
- "Bash(git push *)",
31
- "Bash(rm *)",
32
- "Bash(npm run *)",
33
- "Bash(npx *)"
42
+ "Bash(rm -rf /)",
43
+ "Bash(rm -rf ~)",
44
+ "Bash(rm -rf .)",
45
+ "Bash(sudo rm *)",
46
+ "Bash(sudo rm -rf *)",
47
+ "Bash(git push --force * main)",
48
+ "Bash(git push --force * master)"
34
49
  ]
35
50
  },
36
51
  "hooks": {
@@ -123,6 +138,12 @@
123
138
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
124
139
  "timeout": 2,
125
140
  "async": true
141
+ },
142
+ {
143
+ "type": "command",
144
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-checkpoint.sh",
145
+ "timeout": 5,
146
+ "async": true
126
147
  }
127
148
  ]
128
149
  },
@@ -16,7 +16,25 @@
16
16
  "Bash(npm *)",
17
17
  "Bash(npx *)",
18
18
  "Bash(node *)",
19
- "Bash(git *)"
19
+ "Bash(git *)",
20
+ "Bash(chmod *)",
21
+ "Bash(mkdir *)",
22
+ "Bash(rm /tmp/*)",
23
+ "Bash(cat *)",
24
+ "Bash(ls *)",
25
+ "Bash(echo *)",
26
+ "Bash(printf *)",
27
+ "Bash(node --check *)",
28
+ "Bash(bash -n *)"
29
+ ],
30
+ "deny": [
31
+ "Bash(rm -rf /)",
32
+ "Bash(rm -rf ~)",
33
+ "Bash(rm -rf .)",
34
+ "Bash(sudo rm *)",
35
+ "Bash(sudo rm -rf *)",
36
+ "Bash(git push --force * main)",
37
+ "Bash(git push --force * master)"
20
38
  ]
21
39
  },
22
40
  "hooks": {
@@ -109,6 +127,12 @@
109
127
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/watchtower-send.sh PostToolUse",
110
128
  "timeout": 2,
111
129
  "async": true
130
+ },
131
+ {
132
+ "type": "command",
133
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-checkpoint.sh",
134
+ "timeout": 5,
135
+ "async": true
112
136
  }
113
137
  ]
114
138
  },
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: auto-checkpoint
3
+ description: >
4
+ Toggle automatic checkpoints, or change how often they fire. When on,
5
+ UV Suite writes a checkpoint every N minutes (default 10): a mechanical
6
+ state snapshot from the PostToolUse hook, plus a semantic summary written
7
+ by Claude (via `claude -p --bare --model haiku`) from the Watchtower's
8
+ timer. Sessions with no activity in the window are skipped.
9
+ argument-hint: "[on|off|<minutes>|status]"
10
+ user-invocable: true
11
+ allowed-tools:
12
+ - Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/auto-checkpoint-helper.sh *)
13
+ ---
14
+
15
+ ## Apply /auto-checkpoint $ARGUMENTS
16
+
17
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/auto-checkpoint-helper.sh $ARGUMENTS`
18
+
19
+ ## Instructions
20
+
21
+ Show the user the line of output above as the response — it confirms what
22
+ changed. Do not add commentary. The change applies to the very next interval;
23
+ no restart needed.
24
+
25
+ ## What this controls
26
+
27
+ - `on` / `off` — enable or disable both tiers (mechanical + semantic).
28
+ - `<minutes>` — set the checkpoint interval (1-1440). Default 10.
29
+ - `status` (or no argument) — print the current mode and interval.
30
+
31
+ ## How it works
32
+
33
+ - **Tier A (mechanical):** the `auto-checkpoint.sh` hook runs after each tool
34
+ call. When the interval has passed and there's been activity since the last
35
+ checkpoint, it writes a deterministic snapshot — git state, recent tool calls,
36
+ files touched — to `uv-out/checkpoints/<sid>/auto-<ts>-mechanical.md`.
37
+ - **Tier B (semantic):** the Watchtower process (`uvs watch`) keeps a timer.
38
+ Every N minutes, for each active session, it shells out to `claude -p --bare
39
+ --model haiku` with a prompt assembled from the recent dashboard events and
40
+ git state. Output is saved next to the mechanical checkpoint as
41
+ `auto-<ts>-semantic.md`. Cap: `--max-budget-usd 0.05` per call.
42
+ - Both tiers fire `AutoCheckpoint` events to the Watchtower so they show up as
43
+ distinct rows on the dashboard.
44
+ - Sessions with zero activity in the interval are skipped — no empty checkpoints.
45
+
46
+ State lives in `.uv-suite-state/auto-checkpoint.json` (mode + interval) and
47
+ `.uv-suite-state/sessions/<sid>.last-{mechanical,semantic}-checkpoint.txt`.
@@ -7,15 +7,12 @@ description: >
7
7
  argument-hint: "[on|off|<number>|status]"
8
8
  user-invocable: true
9
9
  allowed-tools:
10
- - Bash(mkdir *)
11
- - Bash(echo *)
12
- - Bash(cat *)
13
- - Bash(printf *)
10
+ - Bash("$CLAUDE_PROJECT_DIR"/.claude/hooks/confirm-helper.sh *)
14
11
  ---
15
12
 
16
13
  ## Apply /confirm $ARGUMENTS
17
14
 
18
- !`STATE_DIR="${CLAUDE_PROJECT_DIR:-.}/.uv-suite-state"; mkdir -p "$STATE_DIR"; MODE_FILE="$STATE_DIR/confirm-mode.txt"; THRESH_FILE="$STATE_DIR/confirm-threshold.txt"; ARG=$(printf '%s' "$ARGUMENTS" | tr -d '[:space:]'); current_mode() { cat "$MODE_FILE" 2>/dev/null || echo "on"; }; current_thresh() { cat "$THRESH_FILE" 2>/dev/null || echo "50"; }; case "$ARG" in on) echo "on" > "$MODE_FILE"; echo "Confirm mode: ON (threshold: $(current_thresh) words)";; off) echo "off" > "$MODE_FILE"; echo "Confirm mode: OFF";; ''|status) echo "Confirm mode: $(current_mode) (threshold: $(current_thresh) words)";; *) if printf '%s' "$ARG" | grep -qE '^[0-9]+$'; then echo "$ARG" > "$THRESH_FILE"; echo "Threshold set to $ARG words (mode: $(current_mode))"; else echo "Usage: /confirm [on | off | <number> | status]"; fi;; esac`
15
+ !`"$CLAUDE_PROJECT_DIR"/.claude/hooks/confirm-helper.sh $ARGUMENTS`
19
16
 
20
17
  ## Instructions
21
18
 
@@ -31,5 +28,5 @@ next user prompt; no restart needed.
31
28
  step. Slash commands (`/foo ...`) are always exempt.
32
29
  - `status` (or no argument) — print the current mode and threshold.
33
30
 
34
- State lives in `${CLAUDE_PROJECT_DIR}/.uv-suite-state/confirm-mode.txt` and
35
- `confirm-threshold.txt`. Defaults if missing: mode `on`, threshold `50`.
31
+ State lives in `.uv-suite-state/confirm-mode.txt` and `.uv-suite-state/confirm-threshold.txt`
32
+ under `$CLAUDE_PROJECT_DIR`. Defaults if missing: mode `on`, threshold `50`.