rhachet-roles-ehmpathy 1.17.12 → 1.17.13

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,81 @@
1
+ ### .rule = forbid-term-script
2
+
3
+ #### .what
4
+ the term `script` is forbidden — it is overloaded and vague
5
+
6
+ #### .scope
7
+ - code: variable names, function names, comments
8
+ - files: filenames, directory names
9
+ - docs: markdown, briefs, prompts
10
+ - comms: commit messages, pr descriptions
11
+
12
+ #### .why
13
+ - `script` means too many things — it carries no precise signal
14
+ - "all code is scripted" — the term adds no information
15
+ - forces lazy categorization instead of domain clarity
16
+ - hides the actual purpose and lifecycle of the code
17
+
18
+ #### .enforcement
19
+ use of `script` = **BLOCKER**
20
+
21
+ #### .alternatives
22
+
23
+ | 👎 vague | 👍 precise | .when |
24
+ | -------- | ----------- | -------------------------------- |
25
+ | `script` | `command` | shell-invoked, adhoc usage |
26
+ | `script` | `procedure` | reusable sequence of steps |
27
+ | `script` | `operation` | domain logic with business rules |
28
+ | `script` | `task` | background or scheduled work |
29
+ | `script` | `migration` | database schema or data changes |
30
+ | `script` | `hook` | lifecycle callback |
31
+ | `script` | `skill` | agent-invoked capability |
32
+ | `script` | `init` | setup or bootstrap logic |
33
+ | `script` | `transform` | data shape conversion |
34
+ | `script` | `handler` | event or request responder |
35
+
36
+ #### .examples
37
+
38
+ **👎 bad**
39
+ ```
40
+ scripts/
41
+ deploy-script.sh
42
+ cleanup-script.js
43
+ data-script.ts
44
+ ```
45
+
46
+ ```ts
47
+ // run the script to fix the data
48
+ const runScript = () => { ... };
49
+ ```
50
+
51
+ **👍 good**
52
+ ```
53
+ commands/
54
+ deploy.sh
55
+ cleanup.command.ts
56
+ migrations/
57
+ 2024-01-fix-user-data.ts
58
+ ```
59
+
60
+ ```ts
61
+ // run the migration to fix the data
62
+ const runMigration = () => { ... };
63
+
64
+ // invoke the command to deploy
65
+ const invokeDeployCommand = () => { ... };
66
+ ```
67
+
68
+ #### .note: filenames
69
+ common renames:
70
+
71
+ | 👎 vague | 👍 precise |
72
+ | ----------------- | --------------------- |
73
+ | `scripts/` | `commands/` or `bin/` |
74
+ | `run-script.sh` | `run.command.sh` |
75
+ | `build-script.js` | `build.command.js` |
76
+ | `test-script.ts` | `test.command.ts` |
77
+ | `setup-script.sh` | `init.sh` |
78
+
79
+ #### .see also
80
+ - `rule.require.ubiqlang` — use precise domain terms
81
+ - `rule.require.treestruct` — name structure guidance
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = PreToolUse hook to forbid terms from a configurable blocklist
4
+ #
5
+ # .why = certain terms are overloaded or vague and degrade precision.
6
+ # this hook blocks Write and Edit operations that contain
7
+ # blocklisted terms, via the HARDNUDGE pattern (block first,
8
+ # allow retry).
9
+ #
10
+ # .how = reads JSON from stdin, extracts content from Write/Edit,
11
+ # loads terms.blocklist.jsonc, scans for matches, blocks
12
+ # on first attempt but allows retry within 5 minutes.
13
+ #
14
+ # usage:
15
+ # configure in .claude/settings.json under hooks.PreToolUse
16
+ #
17
+ # guarantee:
18
+ # - blocks blocklisted terms on first attempt
19
+ # - allows retry within 5 min window (HARDNUDGE)
20
+ # - shows why term is forbidden and alternatives
21
+ ######################################################################
22
+
23
+ set -euo pipefail
24
+
25
+ # config
26
+ HARDNUDGE_WINDOW_SECONDS=300 # 5 minutes
27
+ STALE_THRESHOLD_SECONDS=3600 # 1 hour
28
+
29
+ # read JSON from stdin
30
+ STDIN_INPUT=$(cat)
31
+
32
+ # failfast: if no input, error
33
+ if [[ -z "$STDIN_INPUT" ]]; then
34
+ echo "ERROR: PreToolUse hook received no input via stdin" >&2
35
+ exit 2
36
+ fi
37
+
38
+ # extract tool name
39
+ TOOL_NAME=$(echo "$STDIN_INPUT" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
40
+
41
+ # skip if not Write or Edit
42
+ if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
43
+ exit 0
44
+ fi
45
+
46
+ # extract file path
47
+ FILE_PATH=$(echo "$STDIN_INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
48
+
49
+ # extract content to scan based on tool type
50
+ if [[ "$TOOL_NAME" == "Write" ]]; then
51
+ CONTENT=$(echo "$STDIN_INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null || echo "")
52
+ else
53
+ # Edit: only scan new_string (additions, not removals)
54
+ CONTENT=$(echo "$STDIN_INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null || echo "")
55
+ fi
56
+
57
+ # skip if no content
58
+ if [[ -z "$CONTENT" ]]; then
59
+ exit 0
60
+ fi
61
+
62
+ # find hook directory for blocklist config
63
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
64
+ BLOCKLIST_FILE="$HOOK_DIR/terms.blocklist.jsonc"
65
+
66
+ # skip if no blocklist config
67
+ if [[ ! -f "$BLOCKLIST_FILE" ]]; then
68
+ exit 0
69
+ fi
70
+
71
+ # load blocklist config (strip comments, parse JSON)
72
+ TERMS_JSON=$(sed 's|//.*||' "$BLOCKLIST_FILE" | jq -c '.terms // []' 2>/dev/null || echo "[]")
73
+
74
+ # skip if no terms
75
+ if [[ "$TERMS_JSON" == "[]" ]]; then
76
+ exit 0
77
+ fi
78
+
79
+ # find .claude directory
80
+ find_claude_dir() {
81
+ local dir="$PWD"
82
+ while [[ "$dir" != "/" ]]; do
83
+ if [[ -d "$dir/.claude" ]]; then
84
+ echo "$dir/.claude"
85
+ return 0
86
+ fi
87
+ dir="$(dirname "$dir")"
88
+ done
89
+ return 1
90
+ }
91
+
92
+ CLAUDE_DIR=$(find_claude_dir) || {
93
+ mkdir -p "$PWD/.claude"
94
+ CLAUDE_DIR="$PWD/.claude"
95
+ }
96
+
97
+ NUDGE_FILE="$CLAUDE_DIR/terms.blocklist.nudges.local.json"
98
+
99
+ # ensure nudge file exists
100
+ if [[ ! -f "$NUDGE_FILE" ]]; then
101
+ echo '{}' > "$NUDGE_FILE"
102
+ fi
103
+
104
+ # cleanup stale entries (older than 1 hour)
105
+ # nudge format: { hash: { time, path, terms } }
106
+ NOW=$(date +%s)
107
+ TMP_FILE=$(mktemp)
108
+ jq --argjson now "$NOW" --argjson threshold "$STALE_THRESHOLD_SECONDS" \
109
+ 'to_entries | map(select(.value.time > ($now - $threshold))) | from_entries' \
110
+ "$NUDGE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$NUDGE_FILE" || rm -f "$TMP_FILE"
111
+
112
+ # detect matching terms
113
+ DETECTED_TERMS=""
114
+ DETECTED_INFO=""
115
+ term_count=$(echo "$TERMS_JSON" | jq 'length')
116
+ for ((i=0; i<term_count; i++)); do
117
+ TERM=$(echo "$TERMS_JSON" | jq -r ".[$i].term")
118
+ WHY=$(echo "$TERMS_JSON" | jq -r ".[$i].why")
119
+ ALT=$(echo "$TERMS_JSON" | jq -r ".[$i].alt | join(\", \")")
120
+
121
+ # case-insensitive word boundary match
122
+ if echo "$CONTENT" | grep -iqE "\\b${TERM}\\b"; then
123
+ if [[ -n "$DETECTED_TERMS" ]]; then
124
+ DETECTED_TERMS="${DETECTED_TERMS},\"${TERM}\""
125
+ else
126
+ DETECTED_TERMS="\"${TERM}\""
127
+ fi
128
+ DETECTED_INFO="${DETECTED_INFO} ⛔ ${TERM}\n why: ${WHY}\n alt: ${ALT}\n"
129
+ fi
130
+ done
131
+
132
+ # if no terms detected, allow
133
+ if [[ -z "$DETECTED_TERMS" ]]; then
134
+ exit 0
135
+ fi
136
+
137
+ # build nudge key as hash of file_path
138
+ NUDGE_KEY=$(echo -n "${FILE_PATH}" | sha256sum | cut -d' ' -f1)
139
+
140
+ # check last attempt time (nudge format: { hash: { time, path, terms } })
141
+ LAST_ATTEMPT=$(jq -r --arg key "$NUDGE_KEY" '.[$key].time // 0' "$NUDGE_FILE" 2>/dev/null || echo "0")
142
+ elapsed=$((NOW - LAST_ATTEMPT))
143
+
144
+ if [[ $elapsed -lt $HARDNUDGE_WINDOW_SECONDS ]]; then
145
+ # within retry window, allow
146
+ exit 0
147
+ fi
148
+
149
+ # first attempt - record and block
150
+ TMP_FILE=$(mktemp)
151
+ jq --arg key "$NUDGE_KEY" --argjson time "$NOW" --arg path "$FILE_PATH" --argjson terms "[${DETECTED_TERMS}]" \
152
+ '. + {($key): {time: $time, path: $path, terms: $terms}}' \
153
+ "$NUDGE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$NUDGE_FILE" || rm -f "$TMP_FILE"
154
+
155
+ # build block message
156
+ {
157
+ echo ""
158
+ echo "🛑 BLOCKED: forbidden term(s) detected in file write"
159
+ echo ""
160
+ echo "file: $FILE_PATH"
161
+ echo ""
162
+ echo "detected terms:"
163
+ echo -e "$DETECTED_INFO"
164
+ echo "see rule.forbid.term-* briefs for rationale."
165
+ echo ""
166
+ echo "if this is intentional and absolutely unavoidable, retry the same operation."
167
+ echo ""
168
+ } >&2
169
+
170
+ exit 2
@@ -60,7 +60,7 @@ fi
60
60
 
61
61
  # find script directory for allowlist
62
62
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63
- ALLOWLIST_FILE="$SCRIPT_DIR/gerunds.allowlist.jsonc"
63
+ ALLOWLIST_FILE="$SCRIPT_DIR/terms.gerunds.allowlist.jsonc"
64
64
 
65
65
  # load allowlist (strip comments, extract all words)
66
66
  ALLOWLIST=()
@@ -123,7 +123,7 @@ CLAUDE_DIR=$(find_claude_dir) || {
123
123
  CLAUDE_DIR="$PWD/.claude"
124
124
  }
125
125
 
126
- NUDGE_FILE="$CLAUDE_DIR/gerund.nudges.local.json"
126
+ NUDGE_FILE="$CLAUDE_DIR/terms.gerunds.nudges.local.json"
127
127
 
128
128
  # ensure nudge file exists
129
129
  if [[ ! -f "$NUDGE_FILE" ]]; then
@@ -131,37 +131,46 @@ if [[ ! -f "$NUDGE_FILE" ]]; then
131
131
  fi
132
132
 
133
133
  # cleanup stale entries (older than 1 hour)
134
+ # nudge format: { hash: { time, path, terms } }
134
135
  NOW=$(date +%s)
135
136
  TMP_FILE=$(mktemp)
136
137
  jq --argjson now "$NOW" --argjson threshold "$STALE_THRESHOLD_SECONDS" \
137
- 'to_entries | map(select(.value > ($now - $threshold))) | from_entries' \
138
+ 'to_entries | map(select(.value.time > ($now - $threshold))) | from_entries' \
138
139
  "$NUDGE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$NUDGE_FILE" || rm -f "$TMP_FILE"
139
140
 
140
- # check each gerund against nudge file
141
- BLOCKED_GERUNDS=()
142
- for gerund in "${GERUNDS[@]}"; do
143
- # build nudge key as hash of file_path + gerund
144
- NUDGE_KEY=$(echo -n "${FILE_PATH}:${gerund}" | sha256sum | cut -d' ' -f1)
141
+ # build nudge key as hash of file_path
142
+ NUDGE_KEY=$(echo -n "${FILE_PATH}" | sha256sum | cut -d' ' -f1)
145
143
 
146
- # check last attempt time
147
- LAST_ATTEMPT=$(jq -r --arg key "$NUDGE_KEY" '.[$key] // 0' "$NUDGE_FILE" 2>/dev/null || echo "0")
148
- ELAPSED=$((NOW - LAST_ATTEMPT))
144
+ # check last attempt time (nudge format: { hash: { time, path, terms } })
145
+ LAST_ATTEMPT=$(jq -r --arg key "$NUDGE_KEY" '.[$key].time // 0' "$NUDGE_FILE" 2>/dev/null || echo "0")
146
+ ELAPSED=$((NOW - LAST_ATTEMPT))
149
147
 
150
- if [[ $ELAPSED -lt $HARDNUDGE_WINDOW_SECONDS ]]; then
151
- # within retry window, allow this gerund
152
- continue
153
- fi
148
+ if [[ $ELAPSED -lt $HARDNUDGE_WINDOW_SECONDS ]]; then
149
+ # within retry window, allow
150
+ exit 0
151
+ fi
154
152
 
155
- # outside window, record attempt and block
156
- TMP_FILE=$(mktemp)
157
- jq --arg key "$NUDGE_KEY" --argjson ts "$NOW" '. + {($key): $ts}' "$NUDGE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$NUDGE_FILE" || rm -f "$TMP_FILE"
158
- BLOCKED_GERUNDS+=("$gerund")
153
+ # outside window - all detected gerunds are blocked
154
+ BLOCKED_GERUNDS=("${GERUNDS[@]}")
155
+
156
+ # build terms array for nudge record
157
+ TERMS_JSON="["
158
+ first=true
159
+ for gerund in "${BLOCKED_GERUNDS[@]}"; do
160
+ if [[ "$first" == "true" ]]; then
161
+ TERMS_JSON="${TERMS_JSON}\"${gerund}\""
162
+ first=false
163
+ else
164
+ TERMS_JSON="${TERMS_JSON},\"${gerund}\""
165
+ fi
159
166
  done
167
+ TERMS_JSON="${TERMS_JSON}]"
160
168
 
161
- # if all gerunds are within retry window, allow
162
- if [[ ${#BLOCKED_GERUNDS[@]} -eq 0 ]]; then
163
- exit 0
164
- fi
169
+ # record attempt with new format: { hash: { time, path, terms } }
170
+ TMP_FILE=$(mktemp)
171
+ jq --arg key "$NUDGE_KEY" --argjson time "$NOW" --arg path "$FILE_PATH" --argjson terms "$TERMS_JSON" \
172
+ '. + {($key): {time: $time, path: $path, terms: $terms}}' \
173
+ "$NUDGE_FILE" > "$TMP_FILE" 2>/dev/null && mv "$TMP_FILE" "$NUDGE_FILE" || rm -f "$TMP_FILE"
165
174
 
166
175
  # build block message
167
176
  {
@@ -0,0 +1,11 @@
1
+ {
2
+ // forbidden terms blocklist
3
+ // each entry specifies a term to forbid with rationale and alternatives
4
+ "terms": [
5
+ {
6
+ "term": "script",
7
+ "why": "overloaded and vague - conflates command, procedure, operation, and mechanism",
8
+ "alt": ["command", "executable", "procedure", "operation", "mechanism"]
9
+ }
10
+ ]
11
+ }
@@ -7,6 +7,7 @@
7
7
  # • SessionStart: notify Claude of allowed permissions upfront
8
8
  # • PreToolUse: forbid stderr redirects (2>&1)
9
9
  # • PreToolUse: forbid gerunds in file writes (HARDNUDGE)
10
+ # • PreToolUse: forbid blocklisted terms in file writes (HARDNUDGE)
10
11
  # • PreToolUse: check permissions before new requests
11
12
  #
12
13
  # this script manages hook registration via findsert utility.
@@ -85,18 +86,32 @@ run_findsert "pretooluse.forbid-stderr-redirect" \
85
86
  --timeout 5 \
86
87
  --position prepend
87
88
 
88
- run_findsert "pretooluse.forbid-gerunds.write" \
89
+ run_findsert "pretooluse.forbid-terms.gerunds.write" \
89
90
  --hook-type PreToolUse \
90
91
  --matcher "Write" \
91
- --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-gerunds" \
92
- --name "pretooluse.forbid-gerunds.write" \
92
+ --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-terms.gerunds" \
93
+ --name "pretooluse.forbid-terms.gerunds.write" \
93
94
  --timeout 5
94
95
 
95
- run_findsert "pretooluse.forbid-gerunds.edit" \
96
+ run_findsert "pretooluse.forbid-terms.gerunds.edit" \
96
97
  --hook-type PreToolUse \
97
98
  --matcher "Edit" \
98
- --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-gerunds" \
99
- --name "pretooluse.forbid-gerunds.edit" \
99
+ --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-terms.gerunds" \
100
+ --name "pretooluse.forbid-terms.gerunds.edit" \
101
+ --timeout 5
102
+
103
+ run_findsert "pretooluse.forbid-terms.blocklist.write" \
104
+ --hook-type PreToolUse \
105
+ --matcher "Write" \
106
+ --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-terms.blocklist" \
107
+ --name "pretooluse.forbid-terms.blocklist.write" \
108
+ --timeout 5
109
+
110
+ run_findsert "pretooluse.forbid-terms.blocklist.edit" \
111
+ --hook-type PreToolUse \
112
+ --matcher "Edit" \
113
+ --command "$RHACHET_INIT claude.hooks/pretooluse.forbid-terms.blocklist" \
114
+ --name "pretooluse.forbid-terms.blocklist.edit" \
100
115
  --timeout 5
101
116
 
102
117
  run_findsert "pretooluse.check-permissions" \
@@ -9,6 +9,9 @@
9
9
  "permissions": {
10
10
  // commands that should never be auto-approved
11
11
  "deny": [
12
+ // whatever it uses via bash is typically executable via its own name -> own permissions
13
+ "Bash(bash:*)",
14
+
12
15
  // git write operations - require explicit user request for audit trail
13
16
  "Bash(git commit:*)",
14
17
  "Bash(git add .)",
@@ -107,7 +110,6 @@
107
110
 
108
111
  // commands that require explicit user approval each time
109
112
  "ask": [
110
- "Bash(bash:*)",
111
113
  "Bash(chmod:*)",
112
114
  "Bash(npm install:*)",
113
115
  "Bash(pnpm install:*)",
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.17.12",
5
+ "version": "1.17.13",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [
@@ -49,7 +49,7 @@
49
49
  "prepare:husky": "husky install && chmod ug+x .husky/*",
50
50
  "prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
51
51
  "test:format:biome": "biome format",
52
- "prepare:rhachet": "rhachet init && rhachet roles link --role mechanic && rhachet roles init --role mechanic"
52
+ "prepare:rhachet": "rhachet init && rhachet roles link --role mechanic && rhachet roles init --role mechanic && rhachet roles link --role behaver && rhachet roles init --role behaver"
53
53
  },
54
54
  "dependencies": {
55
55
  "@ehmpathy/as-command": "1.0.3",
@@ -89,9 +89,9 @@
89
89
  "esbuild-register": "3.6.0",
90
90
  "husky": "8.0.3",
91
91
  "jest": "30.2.0",
92
- "rhachet": "1.20.5",
93
- "rhachet-roles-bhrain": "0.5.1",
94
- "rhachet-roles-bhuild": "0.5.4",
92
+ "rhachet": "1.20.7",
93
+ "rhachet-roles-bhrain": "0.5.9",
94
+ "rhachet-roles-bhuild": "0.5.6",
95
95
  "rhachet-roles-ehmpathy": "link:.",
96
96
  "test-fns": "1.6.0",
97
97
  "tsc-alias": "1.8.10",
@@ -1,7 +0,0 @@
1
- never use the term `script`
2
-
3
- it's overloaded
4
-
5
- choose what exactly its purpose is
6
-
7
- - adhoc usage? => `command`
@@ -1,9 +0,0 @@
1
- pitch: "script" is overloaded
2
-
3
- specify what it is
4
- - command? = shell invoked only
5
- - procedure? = reusable sequence of steps
6
- - operation? = domain logic procedure
7
- - etc
8
-
9
- all is scripted. be specific with what it is