rhachet-roles-ehmpathy 1.13.3 → 1.13.7

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.
@@ -107,29 +107,28 @@ mapfile -t ALLOWED_PATTERNS < <(
107
107
  jq -r '.permissions.allow // [] | .[] | select(startswith("Bash(")) | sub("^Bash\\("; "") | sub("\\)$"; "")' "$SETTINGS_FILE" 2>/dev/null
108
108
  )
109
109
 
110
- # Check if command matches any allowed pattern
110
+ # Check if a single command matches any allowed pattern
111
111
  match_pattern() {
112
112
  local cmd="$1"
113
113
  local pattern="$2"
114
114
 
115
115
  # Handle Claude Code's :* suffix matcher
116
- # :* means "optionally match colon and anything after"
117
- # e.g., "npm run test:*" matches "npm run test", "npm run test:", "npm run test:unit"
116
+ # :* means "match anything after" (any suffix, including spaces)
117
+ # e.g., "mkdir:*" matches "mkdir", "mkdir /path", "mkdir -p /foo/bar"
118
+ # e.g., "npm run test:*" matches "npm run test", "npm run test:unit"
118
119
 
119
120
  # First, escape regex special chars except * and :
120
121
  local escaped_pattern
121
122
  escaped_pattern=$(printf '%s' "$pattern" | sed 's/[.^$+?{}()[\]|\\]/\\&/g')
122
123
 
123
124
  # Convert :* to placeholder first (to avoid * -> .* conversion interfering)
124
- # Using a unique placeholder that won't appear in commands
125
- escaped_pattern="${escaped_pattern//:\*/__COLON_STAR_PLACEHOLDER__}"
125
+ escaped_pattern="${escaped_pattern//:\*/__ANY_SUFFIX_PLACEHOLDER__}"
126
126
 
127
127
  # Convert remaining * to .* (glob-style wildcard)
128
128
  escaped_pattern="${escaped_pattern//\*/.*}"
129
129
 
130
- # Now replace placeholder with the actual regex for :*
131
- # (:.*)? matches: nothing, ":", ":foo", ":foo:bar"
132
- escaped_pattern="${escaped_pattern//__COLON_STAR_PLACEHOLDER__/(:.*)?}"
130
+ # Now replace placeholder with .* for "any suffix" matching
131
+ escaped_pattern="${escaped_pattern//__ANY_SUFFIX_PLACEHOLDER__/.*}"
133
132
 
134
133
  # Build final regex
135
134
  local regex="^${escaped_pattern}$"
@@ -140,6 +139,120 @@ match_pattern() {
140
139
  return 1
141
140
  }
142
141
 
142
+ # Check if a single command matches ANY allowed pattern
143
+ command_is_allowed() {
144
+ local cmd="$1"
145
+ # Trim leading/trailing whitespace
146
+ cmd=$(echo "$cmd" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
147
+
148
+ # Empty commands are allowed (e.g., trailing &&)
149
+ if [[ -z "$cmd" ]]; then
150
+ return 0
151
+ fi
152
+
153
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
154
+ if match_pattern "$cmd" "$pattern"; then
155
+ return 0
156
+ fi
157
+ done
158
+ return 1
159
+ }
160
+
161
+ # Split compound command on &&, ||, ; (respecting quotes)
162
+ # Returns newline-separated list of commands
163
+ split_compound_command() {
164
+ local input="$1"
165
+ local result=""
166
+ local current=""
167
+ local in_single_quote=false
168
+ local in_double_quote=false
169
+ local i=0
170
+ local len=${#input}
171
+
172
+ while [[ $i -lt $len ]]; do
173
+ local char="${input:$i:1}"
174
+ local next_char="${input:$((i+1)):1}"
175
+
176
+ # Handle quotes
177
+ if [[ "$char" == "'" && "$in_double_quote" == false ]]; then
178
+ in_single_quote=$([[ "$in_single_quote" == true ]] && echo false || echo true)
179
+ current+="$char"
180
+ ((i++))
181
+ continue
182
+ fi
183
+
184
+ if [[ "$char" == '"' && "$in_single_quote" == false ]]; then
185
+ in_double_quote=$([[ "$in_double_quote" == true ]] && echo false || echo true)
186
+ current+="$char"
187
+ ((i++))
188
+ continue
189
+ fi
190
+
191
+ # Only split if not inside quotes
192
+ if [[ "$in_single_quote" == false && "$in_double_quote" == false ]]; then
193
+ # Check for && or ||
194
+ if [[ "$char" == "&" && "$next_char" == "&" ]] || [[ "$char" == "|" && "$next_char" == "|" ]]; then
195
+ if [[ -n "$result" ]]; then
196
+ result+=$'\n'
197
+ fi
198
+ result+="$current"
199
+ current=""
200
+ ((i+=2))
201
+ continue
202
+ fi
203
+
204
+ # Check for ;
205
+ if [[ "$char" == ";" ]]; then
206
+ if [[ -n "$result" ]]; then
207
+ result+=$'\n'
208
+ fi
209
+ result+="$current"
210
+ current=""
211
+ ((i++))
212
+ continue
213
+ fi
214
+ fi
215
+
216
+ current+="$char"
217
+ ((i++))
218
+ done
219
+
220
+ # Add final command
221
+ if [[ -n "$result" ]]; then
222
+ result+=$'\n'
223
+ fi
224
+ result+="$current"
225
+
226
+ echo "$result"
227
+ }
228
+
229
+ # Check if ALL parts of a compound command are allowed
230
+ all_parts_allowed() {
231
+ local cmd="$1"
232
+ local parts
233
+ local failed_part=""
234
+
235
+ # Split command into parts
236
+ parts=$(split_compound_command "$cmd")
237
+
238
+ # Check each part
239
+ while IFS= read -r part; do
240
+ # Trim whitespace
241
+ part=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
242
+
243
+ # Skip empty parts
244
+ [[ -z "$part" ]] && continue
245
+
246
+ if ! command_is_allowed "$part"; then
247
+ # Store the failed part for error reporting
248
+ echo "$part"
249
+ return 1
250
+ fi
251
+ done <<< "$parts"
252
+
253
+ return 0
254
+ }
255
+
143
256
  # Transform raw permission pattern to compact bracket notation for display
144
257
  format_pattern() {
145
258
  local pattern="$1"
@@ -155,13 +268,11 @@ format_pattern() {
155
268
  fi
156
269
  }
157
270
 
158
- for pattern in "${ALLOWED_PATTERNS[@]}"; do
159
- if match_pattern "$COMMAND" "$pattern"; then
160
- exit 0 # Command matches an allowed pattern
161
- fi
162
- done
271
+ # Check if all parts of the command (including compound commands) are allowed
272
+ FAILED_PART=$(all_parts_allowed "$COMMAND") && exit 0
163
273
 
164
274
  # Command not matched - handle based on mode
275
+ # FAILED_PART contains the first disallowed part of the command
165
276
 
166
277
  # SOFTNUDGE mode: provide guidance but don't block (early return)
167
278
  # Output plain text - no hookSpecificOutput so normal permission flow continues
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = safe mv wrapper that constrains moves to within the repo
4
+ #
5
+ # .why = mv can move/overwrite files anywhere on the filesystem.
6
+ # this wrapper ensures both source and destination resolve
7
+ # to paths within the current working directory (repo root).
8
+ #
9
+ # .how = uses realpath to resolve absolute paths, then validates
10
+ # both are prefixed by $PWD before executing mv.
11
+ #
12
+ # usage:
13
+ # bash mvsafe.sh <source> <destination>
14
+ #
15
+ # guarantee:
16
+ # ✔ fails if source is outside repo
17
+ # ✔ fails if destination is outside repo
18
+ # ✔ fails if source doesn't exist
19
+ # ✔ passes all arguments to mv if validation passes
20
+ ######################################################################
21
+
22
+ set -euo pipefail
23
+
24
+ if [[ $# -lt 2 ]]; then
25
+ echo "error: mvsafe requires at least 2 arguments" >&2
26
+ echo "usage: mvsafe.sh <source> <destination>" >&2
27
+ exit 1
28
+ fi
29
+
30
+ REPO_ROOT="$PWD"
31
+
32
+ # get the last argument (destination)
33
+ DEST="${*: -1}"
34
+
35
+ # get all arguments except the last (sources, could be multiple)
36
+ SOURCES=("${@:1:$#-1}")
37
+
38
+ # resolve destination path
39
+ # if dest doesn't exist yet, resolve its parent directory
40
+ if [[ -e "$DEST" ]]; then
41
+ DEST_RESOLVED="$(realpath "$DEST")"
42
+ else
43
+ DEST_PARENT="$(dirname "$DEST")"
44
+ if [[ ! -d "$DEST_PARENT" ]]; then
45
+ echo "error: destination parent directory does not exist: $DEST_PARENT" >&2
46
+ exit 1
47
+ fi
48
+ DEST_RESOLVED="$(realpath "$DEST_PARENT")/$(basename "$DEST")"
49
+ fi
50
+
51
+ # validate destination is within repo
52
+ if [[ "$DEST_RESOLVED" != "$REPO_ROOT"* ]]; then
53
+ echo "error: destination is outside repo: $DEST_RESOLVED" >&2
54
+ echo " repo root: $REPO_ROOT" >&2
55
+ exit 1
56
+ fi
57
+
58
+ # validate each source is within repo
59
+ for SRC in "${SOURCES[@]}"; do
60
+ if [[ ! -e "$SRC" ]]; then
61
+ echo "error: source does not exist: $SRC" >&2
62
+ exit 1
63
+ fi
64
+
65
+ SRC_RESOLVED="$(realpath "$SRC")"
66
+
67
+ if [[ "$SRC_RESOLVED" != "$REPO_ROOT"* ]]; then
68
+ echo "error: source is outside repo: $SRC_RESOLVED" >&2
69
+ echo " repo root: $REPO_ROOT" >&2
70
+ exit 1
71
+ fi
72
+ done
73
+
74
+ # all validations passed, execute mv
75
+ exec mv "$@"
@@ -38,7 +38,10 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
38
38
  "Bash(sed:*)",
39
39
  "Bash(tee:*)",
40
40
  "Bash(find:*)",
41
- "Bash(echo:*)"
41
+ "Bash(echo:*)",
42
+ "Bash(mv:*)",
43
+ "Bash(npx biome:*)",
44
+ "Bash(npx jest:*)",
42
45
  ],
43
46
  "ask": [
44
47
  "Bash(bash:*)",
@@ -55,7 +58,6 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
55
58
  "WebFetch(domain:raw.githubusercontent.com)",
56
59
  "WebFetch(domain:biomejs.dev)",
57
60
 
58
-
59
61
  "Bash(ls:*)",
60
62
  "Bash(tree:*)",
61
63
  "Bash(cat:*)",
@@ -68,15 +70,13 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
68
70
  "Bash(file:*)",
69
71
  "Bash(mkdir:*)",
70
72
  "Bash(pwd)",
73
+ "Bash(bash src/logic/roles/mechanic/.skills/claude.tools/mvsafe.sh:*)",
71
74
  "Bash(npm view:*)",
72
75
  "Bash(npm list:*)",
73
76
  "Bash(npm remove:*)",
74
77
 
75
78
  "Bash(npx rhachet roles boot --repo ehmpathy --role mechanic)",
76
79
  "Bash(npx tsx ./bin/run:*)",
77
- "Bash(npx tsc:*)",
78
- "Bash(npx biome:*)",
79
- "Bash(npx jest:*)",
80
80
 
81
81
  "Bash(npm run build:*)",
82
82
  "Bash(npm run build:compile)",
@@ -92,6 +92,9 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
92
92
  "Bash(npm run test:acceptance:*)",
93
93
 
94
94
  "Bash(THOROUGH=true npm run test:*)",
95
+ "Bash(THOROUGH=true npm run test:types:*)",
96
+ "Bash(THOROUGH=true npm run test:format:*)",
97
+ "Bash(THOROUGH=true npm run test:lint:*)",
95
98
  "Bash(THOROUGH=true npm run test:unit:*)",
96
99
  "Bash(THOROUGH=true npm run test:integration:*)",
97
100
  "Bash(THOROUGH=true npm run test:acceptance:*)",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "rhachet-roles-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "empathetic software construction roles and skills, via rhachet",
5
- "version": "1.13.3",
5
+ "version": "1.13.7",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [
@@ -34,8 +34,8 @@
34
34
  "test:format": "npm run test:format:biome",
35
35
  "test:lint:deps": "npx depcheck -c ./.depcheckrc.yml",
36
36
  "test:integration:hold": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
37
- "test:lint:biome": "biome check src --diagnostic-level=error",
38
- "test:lint:biome:all": "biome check src",
37
+ "test:lint:biome": "biome check --diagnostic-level=error",
38
+ "test:lint:biome:all": "biome check",
39
39
  "test:lint": "npm run test:lint:biome && npm run test:lint:deps",
40
40
  "test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main')",
41
41
  "test:integration": "echo 'todo: release'",
@@ -48,13 +48,13 @@
48
48
  "postversion": "git push origin HEAD --tags --no-verify",
49
49
  "prepare:husky": "npx husky install && chmod ug+x .husky/*",
50
50
  "prepare": "[ -e .git ] && npm run prepare:husky || exit 0",
51
- "test:format:biome": "biome format src"
51
+ "test:format:biome": "biome format"
52
52
  },
53
53
  "dependencies": {
54
54
  "@ehmpathy/as-command": "1.0.3",
55
55
  "@ehmpathy/uni-time": "1.8.1",
56
56
  "as-procedure": "1.1.7",
57
- "domain-objects": "0.31.3",
57
+ "domain-objects": "0.31.7",
58
58
  "fast-glob": "3.3.3",
59
59
  "helpful-errors": "1.5.3",
60
60
  "inquirer": "12.7.0",
@@ -80,9 +80,9 @@
80
80
  "@types/node": "22.15.21",
81
81
  "cz-conventional-changelog": "3.3.0",
82
82
  "declapract": "0.13.0",
83
- "declapract-typescript-ehmpathy": "0.43.11",
84
- "declastruct": "1.4.5",
85
- "declastruct-github": "1.0.5",
83
+ "declapract-typescript-ehmpathy": "0.43.16",
84
+ "declastruct": "1.5.1",
85
+ "declastruct-github": "1.0.7",
86
86
  "depcheck": "1.4.3",
87
87
  "esbuild-register": "3.6.0",
88
88
  "husky": "8.0.3",