rhachet-roles-ehmpathy 1.11.0 → 1.12.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.
@@ -0,0 +1,41 @@
1
+ this is an alias to ./forbid.index.ts.md
2
+
3
+ NEVER do barrel exports
4
+
5
+ e.g.,
6
+ - `src/domain.objects/index.ts`
7
+ - `src/domain.operations/organization/index.ts`
8
+ - `src/domain.operations/organizationAccount/index.ts`
9
+ - `src/access/daos/index.ts`
10
+ - `src/contract/sdks/index.ts`
11
+
12
+ all are banned
13
+
14
+ they're just new aliases that increase
15
+ - codepath variants
16
+ - cyclical import chances
17
+
18
+ totally forbidden
19
+
20
+ they add zero value
21
+
22
+
23
+ ---
24
+
25
+ the only thing that's allowed is
26
+
27
+ within an index.ts file, exporting one object
28
+
29
+ e.g.,
30
+
31
+ ```ts
32
+ export const daoAwsOrganization = {
33
+ getOne,
34
+ getAll,
35
+ set,
36
+ del,
37
+ }
38
+ ```
39
+ nice and tight export
40
+
41
+ but thats it. never just an export forwarder
@@ -0,0 +1,10 @@
1
+ never use the term `existing`; its a needless gerund
2
+
3
+ either
4
+ - foundBefore
5
+ - or
6
+ - foundAfter
7
+
8
+ or some nonGerund alternative
9
+
10
+ but never `existing`
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = PreToolUse hook to encourage reuse of existing permissions
4
+ #
5
+ # .why = when Claude attempts a command not covered by pre-approved
6
+ # permissions, this hook asks it to reconsider whether an
7
+ # existing permission could accomplish the same task.
8
+ #
9
+ # this reduces permission prompts and encourages consistent
10
+ # command patterns across the project.
11
+ #
12
+ # .how = reads JSON from stdin (per Claude Code docs), extracts
13
+ # tool_input.command, checks against allowed patterns.
14
+ # if no match, behavior depends on mode.
15
+ #
16
+ # usage:
17
+ # configure in .claude/settings.local.json under hooks.PreToolUse
18
+ #
19
+ # flags:
20
+ # --mode HARDNUDGE (default) blocks on first attempt, allows on retry
21
+ # tracks attempts in .claude/permissions.attempted.json
22
+ # forces Claude to consciously decide to request
23
+ # a new permission rather than doing so automatically
24
+ #
25
+ # --mode SOFTNUDGE outputs guidance but doesn't block (exit 0)
26
+ # Claude sees the message but can proceed immediately
27
+ #
28
+ # guarantee:
29
+ # ✔ HARDNUDGE (default): blocks first attempt, allows retry
30
+ # ✔ SOFTNUDGE: non-blocking, feedback only
31
+ # ✔ fast: simple pattern matching
32
+ # ✔ helpful: shows available alternatives
33
+ ######################################################################
34
+
35
+ set -euo pipefail
36
+
37
+ # Parse flags
38
+ MODE="HARDNUDGE" # default
39
+ HARDNUDGE_WINDOW_SECONDS=60 # default: 60 seconds
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --mode)
43
+ MODE="${2:-HARDNUDGE}"
44
+ shift 2
45
+ ;;
46
+ --window)
47
+ HARDNUDGE_WINDOW_SECONDS="${2:-60}"
48
+ shift 2
49
+ ;;
50
+ *)
51
+ shift
52
+ ;;
53
+ esac
54
+ done
55
+
56
+ # Read JSON from stdin (Claude Code passes input via stdin, not env var)
57
+ STDIN_INPUT=$(cat)
58
+
59
+ # failfast: if no input received, something is wrong
60
+ if [[ -z "$STDIN_INPUT" ]]; then
61
+ echo "ERROR: PreToolUse hook received no input via stdin" >&2
62
+ exit 2 # exit 2 = blocking error per Claude Code docs
63
+ fi
64
+
65
+ # Extract command from stdin JSON
66
+ # Claude passes: {"tool_name": "Bash", "tool_input": {"command": "..."}}
67
+ COMMAND=$(echo "$STDIN_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")
68
+
69
+ # Skip if not a Bash command or empty
70
+ if [[ -z "$COMMAND" ]]; then
71
+ exit 0
72
+ fi
73
+
74
+ # Find the .claude directory (search upward from current directory)
75
+ find_claude_dir() {
76
+ local dir="$PWD"
77
+ while [[ "$dir" != "/" ]]; do
78
+ if [[ -d "$dir/.claude" ]]; then
79
+ echo "$dir/.claude"
80
+ return 0
81
+ fi
82
+ dir="$(dirname "$dir")"
83
+ done
84
+ return 1
85
+ }
86
+
87
+ # Find the settings file (search upward from current directory)
88
+ find_settings_file() {
89
+ local claude_dir
90
+ claude_dir=$(find_claude_dir) || return 1
91
+ local settings_file="$claude_dir/settings.local.json"
92
+ if [[ -f "$settings_file" ]]; then
93
+ echo "$settings_file"
94
+ return 0
95
+ fi
96
+ return 1
97
+ }
98
+
99
+ SETTINGS_FILE=$(find_settings_file) || {
100
+ # No settings file found, allow command to proceed
101
+ exit 0
102
+ }
103
+
104
+ # Extract Bash permissions from settings file
105
+ # Patterns look like: "Bash(npm run test:*)" -> extract "npm run test:*"
106
+ mapfile -t ALLOWED_PATTERNS < <(
107
+ jq -r '.permissions.allow // [] | .[] | select(startswith("Bash(")) | sub("^Bash\\("; "") | sub("\\)$"; "")' "$SETTINGS_FILE" 2>/dev/null
108
+ )
109
+
110
+ # Check if command matches any allowed pattern
111
+ match_pattern() {
112
+ local cmd="$1"
113
+ local pattern="$2"
114
+
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"
118
+
119
+ # First, escape regex special chars except * and :
120
+ local escaped_pattern
121
+ escaped_pattern=$(printf '%s' "$pattern" | sed 's/[.^$+?{}()[\]|\\]/\\&/g')
122
+
123
+ # 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__}"
126
+
127
+ # Convert remaining * to .* (glob-style wildcard)
128
+ escaped_pattern="${escaped_pattern//\*/.*}"
129
+
130
+ # Now replace placeholder with the actual regex for :*
131
+ # (:.*)? matches: nothing, ":", ":foo", ":foo:bar"
132
+ escaped_pattern="${escaped_pattern//__COLON_STAR_PLACEHOLDER__/(:.*)?}"
133
+
134
+ # Build final regex
135
+ local regex="^${escaped_pattern}$"
136
+
137
+ if [[ "$cmd" =~ $regex ]]; then
138
+ return 0
139
+ fi
140
+ return 1
141
+ }
142
+
143
+ # Transform raw permission pattern to compact bracket notation for display
144
+ format_pattern() {
145
+ local pattern="$1"
146
+
147
+ # Check if pattern ends with :*
148
+ if [[ "$pattern" == *":*" ]]; then
149
+ # Remove :* suffix and format with [p]: label (prefix match)
150
+ local prefix="${pattern%:*}"
151
+ echo "[p]: $prefix"
152
+ else
153
+ # Exact match - format with [e]: label
154
+ echo "[e]: $pattern"
155
+ fi
156
+ }
157
+
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
163
+
164
+ # Command not matched - handle based on mode
165
+
166
+ # SOFTNUDGE mode: provide guidance but don't block (early return)
167
+ # Output plain text - no hookSpecificOutput so normal permission flow continues
168
+ if [[ "$MODE" == "SOFTNUDGE" ]]; then
169
+ echo ""
170
+ echo "⚠️ This command is not covered by existing pre-approved permissions."
171
+ echo ""
172
+ echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
173
+ echo ""
174
+ echo "([e] = exact match, [p] = prefix match)"
175
+ echo ""
176
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
177
+ echo " • $(format_pattern "$pattern")"
178
+ done
179
+ echo ""
180
+ echo "([e] = exact match, [p] = prefix match)"
181
+ echo ""
182
+ echo "If an existing permission pattern can solve your task, use that instead."
183
+ echo "If not, proceed with requesting approval."
184
+ echo ""
185
+ exit 0
186
+ fi
187
+
188
+ # HARDNUDGE mode (default): block on first attempt, allow on retry
189
+ CLAUDE_DIR=$(find_claude_dir) || {
190
+ echo "ERROR: No .claude directory found. This hook requires a .claude directory." >&2
191
+ exit 1
192
+ }
193
+ ATTEMPTED_FILE="$CLAUDE_DIR/permission.nudges.local.json"
194
+
195
+ # Ensure the file exists with valid JSON
196
+ if [[ ! -f "$ATTEMPTED_FILE" ]]; then
197
+ echo '{}' > "$ATTEMPTED_FILE"
198
+ fi
199
+
200
+ # Check if this command was recently attempted
201
+ now=$(date +%s)
202
+ last_attempt=$(jq -r --arg cmd "$COMMAND" '.[$cmd] // 0' "$ATTEMPTED_FILE" 2>/dev/null || echo "0")
203
+ elapsed=$((now - last_attempt))
204
+
205
+ if [[ $elapsed -lt $HARDNUDGE_WINDOW_SECONDS ]]; then
206
+ # Claude already tried within the window - they've thought twice
207
+ # Exit silently with 0 so normal permission flow continues (user gets prompted)
208
+ exit 0
209
+ fi
210
+
211
+ # First attempt - record timestamp and block
212
+ # Use a temp file for atomic update
213
+ tmp_file=$(mktemp)
214
+ jq --arg cmd "$COMMAND" --argjson ts "$now" '. + {($cmd): $ts}' "$ATTEMPTED_FILE" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$ATTEMPTED_FILE"
215
+
216
+ # Output block message to stderr and exit 2 to deny
217
+ {
218
+ echo ""
219
+ echo "🛑 BLOCKED: This command is not covered by existing pre-approved permissions."
220
+ echo ""
221
+ echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
222
+ echo ""
223
+ echo "([e] = exact match, [p] = prefix match)"
224
+ echo ""
225
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
226
+ echo " • $(format_pattern "$pattern")"
227
+ done
228
+ echo ""
229
+ echo "([e] = exact match, [p] = prefix match)"
230
+ echo ""
231
+ echo "If an existing permission pattern can solve your task, use that instead."
232
+ echo "If you've considered the alternatives and still need this specific command, retry it."
233
+ echo ""
234
+ } >&2
235
+ exit 2 # Exit 2 = block with error message
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = bind mechanic PreToolUse hook to Claude settings
4
+ #
5
+ # .why = when Claude attempts a Bash command not covered by existing
6
+ # permissions, this hook provides feedback asking it to
7
+ # reconsider whether a pre-approved command could work instead.
8
+ #
9
+ # this reduces unnecessary permission prompts and encourages
10
+ # consistent command patterns across the project.
11
+ #
12
+ # .how = uses jq to findsert the PreToolUse hook configuration
13
+ # into .claude/settings.local.json
14
+ #
15
+ # guarantee:
16
+ # ✔ creates .claude/settings.local.json if missing
17
+ # ✔ preserves existing settings (permissions, other hooks)
18
+ # ✔ idempotent: no-op if hook already present
19
+ # ✔ fail-fast on errors
20
+ ######################################################################
21
+
22
+ set -euo pipefail
23
+
24
+ PROJECT_ROOT="$PWD"
25
+ SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
26
+ SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/check.pretooluse.permissions.sh"
28
+
29
+ # Verify hook script exists
30
+ if [[ ! -f "$HOOK_SCRIPT" ]]; then
31
+ echo "❌ hook script not found: $HOOK_SCRIPT" >&2
32
+ exit 1
33
+ fi
34
+
35
+ # Define the hook configuration to findsert
36
+ HOOK_CONFIG=$(cat <<EOF
37
+ {
38
+ "hooks": {
39
+ "PreToolUse": [
40
+ {
41
+ "matcher": "Bash",
42
+ "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "$HOOK_SCRIPT",
46
+ "timeout": 5
47
+ }
48
+ ]
49
+ }
50
+ ]
51
+ }
52
+ }
53
+ EOF
54
+ )
55
+
56
+ # Ensure .claude directory exists
57
+ mkdir -p "$(dirname "$SETTINGS_FILE")"
58
+
59
+ # Initialize settings file if it doesn't exist
60
+ if [[ ! -f "$SETTINGS_FILE" ]]; then
61
+ echo "{}" > "$SETTINGS_FILE"
62
+ fi
63
+
64
+ # Findsert: merge the hook configuration if not already present
65
+ jq --argjson hook "$HOOK_CONFIG" '
66
+ # Define the target command for comparison
67
+ def targetCmd: $hook.hooks.PreToolUse[0].hooks[0].command;
68
+
69
+ # Check if hook already exists
70
+ def hookExists:
71
+ (.hooks.PreToolUse // [])
72
+ | map(select(.matcher == "Bash") | .hooks // [])
73
+ | flatten
74
+ | map(.command)
75
+ | any(. == targetCmd);
76
+
77
+ # If hook already exists, return unchanged
78
+ if hookExists then
79
+ .
80
+ else
81
+ # Ensure .hooks exists
82
+ .hooks //= {} |
83
+
84
+ # Ensure .hooks.PreToolUse exists
85
+ .hooks.PreToolUse //= [] |
86
+
87
+ # Check if our matcher already exists
88
+ if (.hooks.PreToolUse | map(.matcher) | index("Bash")) then
89
+ # Matcher exists, add our hook to its hooks array
90
+ .hooks.PreToolUse |= map(
91
+ if .matcher == "Bash" then
92
+ .hooks += $hook.hooks.PreToolUse[0].hooks
93
+ else
94
+ .
95
+ end
96
+ )
97
+ else
98
+ # Matcher does not exist, add the entire entry
99
+ .hooks.PreToolUse += $hook.hooks.PreToolUse
100
+ end
101
+ end
102
+ ' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
103
+
104
+ # Check if any changes were made
105
+ if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
106
+ rm "$SETTINGS_FILE.tmp"
107
+ echo "👌 mechanic PreToolUse hook already bound"
108
+ echo " $SETTINGS_FILE"
109
+ exit 0
110
+ fi
111
+
112
+ # Atomic replace
113
+ mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
114
+
115
+ echo "🔗 mechanic PreToolUse hook bound successfully!"
116
+ echo " $SETTINGS_FILE"
117
+ echo ""
118
+ echo "✨ Claude will now be reminded to check existing permissions before requesting new ones"
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = bind mechanic SessionStart hook to Claude settings
4
+ #
5
+ # .why = the mechanic role needs to boot on every Claude session
6
+ # to ensure project context and briefs are loaded.
7
+ #
8
+ # this script "findserts" (find-or-insert) the SessionStart
9
+ # hook into .claude/settings.local.json, ensuring:
10
+ # • the hook is present after running this skill
11
+ # • no duplication if already present
12
+ # • idempotent: safe to rerun
13
+ #
14
+ # .how = uses jq to merge the SessionStart hook configuration
15
+ # into the existing hooks structure, creating it if absent.
16
+ #
17
+ # guarantee:
18
+ # ✔ creates .claude/settings.local.json if missing
19
+ # ✔ preserves existing settings (permissions, other hooks)
20
+ # ✔ idempotent: no-op if hook already present
21
+ # ✔ fail-fast on errors
22
+ ######################################################################
23
+
24
+ set -euo pipefail
25
+
26
+ PROJECT_ROOT="$PWD"
27
+ SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
28
+
29
+ # Define the hook configuration to findsert
30
+ HOOK_CONFIG=$(cat <<'EOF'
31
+ {
32
+ "hooks": {
33
+ "SessionStart": [
34
+ {
35
+ "matcher": "*",
36
+ "hooks": [
37
+ {
38
+ "type": "command",
39
+ "command": "npx rhachet roles boot --repo ehmpathy --role mechanic",
40
+ "timeout": 60
41
+ }
42
+ ]
43
+ }
44
+ ]
45
+ }
46
+ }
47
+ EOF
48
+ )
49
+
50
+ # Ensure .claude directory exists
51
+ mkdir -p "$(dirname "$SETTINGS_FILE")"
52
+
53
+ # Initialize settings file if it doesn't exist
54
+ if [[ ! -f "$SETTINGS_FILE" ]]; then
55
+ echo "{}" > "$SETTINGS_FILE"
56
+ fi
57
+
58
+ # Findsert: merge the hook configuration if not already present
59
+ # Strategy: deep merge with existing hooks, creating structure if needed
60
+ jq --argjson hook "$HOOK_CONFIG" '
61
+ # Define the target command for comparison
62
+ def targetCmd: "npx rhachet roles boot --repo ehmpathy --role mechanic";
63
+
64
+ # Check if hook already exists
65
+ def hookExists:
66
+ (.hooks.SessionStart // [])
67
+ | map(select(.matcher == "*") | .hooks // [])
68
+ | flatten
69
+ | map(.command)
70
+ | index(targetCmd) != null;
71
+
72
+ # If hook already exists, return unchanged
73
+ if hookExists then
74
+ .
75
+ else
76
+ # Ensure .hooks exists
77
+ .hooks //= {} |
78
+
79
+ # Ensure .hooks.SessionStart exists
80
+ .hooks.SessionStart //= [] |
81
+
82
+ # Check if our matcher already exists
83
+ if (.hooks.SessionStart | map(.matcher) | index("*")) then
84
+ # Matcher exists, add our hook to its hooks array
85
+ .hooks.SessionStart |= map(
86
+ if .matcher == "*" then
87
+ .hooks += $hook.hooks.SessionStart[0].hooks
88
+ else
89
+ .
90
+ end
91
+ )
92
+ else
93
+ # Matcher does not exist, add the entire entry
94
+ .hooks.SessionStart += $hook.hooks.SessionStart
95
+ end
96
+ end
97
+ ' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
98
+
99
+ # Check if any changes were made
100
+ if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
101
+ rm "$SETTINGS_FILE.tmp"
102
+ echo "👌 mechanic SessionStart hook already bound"
103
+ echo " $SETTINGS_FILE"
104
+ exit 0
105
+ fi
106
+
107
+ # Atomic replace
108
+ mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
109
+
110
+ echo "🔗 mechanic SessionStart hook bound successfully!"
111
+ echo " $SETTINGS_FILE"
112
+ echo ""
113
+ echo "✨ next time you start a Claude session, the mechanic will boot automatically"
@@ -1,113 +1,24 @@
1
1
  #!/usr/bin/env bash
2
2
  ######################################################################
3
- # .what = bind mechanic SessionStart hook to Claude settings
3
+ # .what = bind all mechanic hooks to Claude settings
4
4
  #
5
- # .why = the mechanic role needs to boot on every Claude session
6
- # to ensure project context and briefs are loaded.
5
+ # .why = the mechanic role uses multiple hooks:
6
+ # SessionStart: boot mechanic on every session
7
+ # • PreToolUse: check existing permissions before new requests
7
8
  #
8
- # this script "findserts" (find-or-insert) the SessionStart
9
- # hook into .claude/settings.local.json, ensuring:
10
- # • the hook is present after running this skill
11
- # • no duplication if already present
12
- # • idempotent: safe to rerun
9
+ # this script dispatches to each hook initializer.
13
10
  #
14
- # .how = uses jq to merge the SessionStart hook configuration
15
- # into the existing hooks structure, creating it if absent.
11
+ # .how = runs each init.claude.hooks.*.sh script in sequence
16
12
  #
17
13
  # guarantee:
18
- # ✔ creates .claude/settings.local.json if missing
19
- # ✔ preserves existing settings (permissions, other hooks)
20
- # ✔ idempotent: no-op if hook already present
14
+ # ✔ idempotent: safe to rerun
21
15
  # ✔ fail-fast on errors
22
16
  ######################################################################
23
17
 
24
18
  set -euo pipefail
25
19
 
26
- PROJECT_ROOT="$PWD"
27
- SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
20
+ SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
21
 
29
- # Define the hook configuration to findsert
30
- HOOK_CONFIG=$(cat <<'EOF'
31
- {
32
- "hooks": {
33
- "SessionStart": [
34
- {
35
- "matcher": "*",
36
- "hooks": [
37
- {
38
- "type": "command",
39
- "command": "npx rhachet roles boot --repo ehmpathy --role mechanic",
40
- "timeout": 60
41
- }
42
- ]
43
- }
44
- ]
45
- }
46
- }
47
- EOF
48
- )
49
-
50
- # Ensure .claude directory exists
51
- mkdir -p "$(dirname "$SETTINGS_FILE")"
52
-
53
- # Initialize settings file if it doesn't exist
54
- if [[ ! -f "$SETTINGS_FILE" ]]; then
55
- echo "{}" > "$SETTINGS_FILE"
56
- fi
57
-
58
- # Findsert: merge the hook configuration if not already present
59
- # Strategy: deep merge with existing hooks, creating structure if needed
60
- jq --argjson hook "$HOOK_CONFIG" '
61
- # Define the target command for comparison
62
- def targetCmd: "npx rhachet roles boot --repo ehmpathy --role mechanic";
63
-
64
- # Check if hook already exists
65
- def hookExists:
66
- (.hooks.SessionStart // [])
67
- | map(select(.matcher == "*") | .hooks // [])
68
- | flatten
69
- | map(.command)
70
- | index(targetCmd) != null;
71
-
72
- # If hook already exists, return unchanged
73
- if hookExists then
74
- .
75
- else
76
- # Ensure .hooks exists
77
- .hooks //= {} |
78
-
79
- # Ensure .hooks.SessionStart exists
80
- .hooks.SessionStart //= [] |
81
-
82
- # Check if our matcher already exists
83
- if (.hooks.SessionStart | map(.matcher) | index("*")) then
84
- # Matcher exists, add our hook to its hooks array
85
- .hooks.SessionStart |= map(
86
- if .matcher == "*" then
87
- .hooks += $hook.hooks.SessionStart[0].hooks
88
- else
89
- .
90
- end
91
- )
92
- else
93
- # Matcher does not exist, add the entire entry
94
- .hooks.SessionStart += $hook.hooks.SessionStart
95
- end
96
- end
97
- ' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
98
-
99
- # Check if any changes were made
100
- if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
101
- rm "$SETTINGS_FILE.tmp"
102
- echo "👌 mechanic SessionStart hook already bound"
103
- echo " $SETTINGS_FILE"
104
- exit 0
105
- fi
106
-
107
- # Atomic replace
108
- mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
109
-
110
- echo "🔗 mechanic SessionStart hook bound successfully!"
111
- echo " $SETTINGS_FILE"
112
- echo ""
113
- echo "✨ next time you start a Claude session, the mechanic will boot automatically"
22
+ # Dispatch to each hook initializer
23
+ "$SKILLS_DIR/init.claude.hooks.sessionstart.sh"
24
+ "$SKILLS_DIR/init.claude.hooks.pretooluse.sh"
@@ -37,6 +37,7 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
37
37
  "Bash(git commit:*)"
38
38
  ],
39
39
  "ask": [
40
+ "Bash(bash:*)",
40
41
  "Bash(chmod:*)",
41
42
  "Bash(pnpm install:*)",
42
43
  "Bash(pnpm add:*)"
@@ -47,16 +48,40 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
47
48
  "WebFetch(domain:www.npmjs.com)",
48
49
  "WebFetch(domain:hub.docker.com)",
49
50
  "WebFetch(domain:raw.githubusercontent.com)",
50
- "Bash(THOROUGH=true npm run test:*)",
51
- "Bash(AWS_PROFILE=ahbode.dev npm run test:integration:*)",
52
- "Bash(npm run fix:*)",
53
- "Bash(AWS_PROFILE=ahbode.dev npx jest:*)",
54
- "Bash(AWS_PROFILE=ahbode.dev npm run deploy:dev:*)",
55
- "Bash(AWS_PROFILE=ahbode.dev STAGE=dev npm run test:acceptance:*)",
51
+
52
+ "Bash(npm run build:*)",
56
53
  "Bash(npm run start:testdb:*)",
54
+ "Bash(AWS_PROFILE=ahbode.dev npm run deploy:dev:*)",
55
+
57
56
  "Bash(cat:*)",
58
57
  "Bash(unzip:*)",
59
- "Bash(npm view:*)"
58
+ "Bash(npm view:*)",
59
+ "Bash(npm list:*)",
60
+ "Bash(pnpm list:*)",
61
+
62
+ "Bash(npx rhachet roles boot --repo ehmpathy --role mechanic)",
63
+
64
+ "Bash(npm run test:*)",
65
+ "Bash(npm run test:unit:*)",
66
+ "Bash(npm run test:integration:*)",
67
+ "Bash(npm run test:acceptance:*)",
68
+
69
+ "Bash(THOROUGH=true npm run test:*)",
70
+ "Bash(THOROUGH=true npm run test:unit:*)",
71
+ "Bash(THOROUGH=true npm run test:integration:*)",
72
+ "Bash(THOROUGH=true npm run test:acceptance:*)",
73
+
74
+ "Bash(AWS_PROFILE=ahbode.dev npm run test:integration:*)",
75
+ "Bash(AWS_PROFILE=ahbode.dev STAGE=dev npm run test:acceptance:*)",
76
+
77
+ "Bash(npm run fix:*)",
78
+ "Bash(npm run fix:format:*)",
79
+ "Bash(npm run fix:lint:*)",
80
+ "Bash(npm run fix:types:*)",
81
+
82
+ "Bash(find:*)",
83
+
84
+ "Bash(source .agent/repo=.this/skills/*)"
60
85
  ]
61
86
  }
62
87
  }
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.11.0",
5
+ "version": "1.12.1",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  "fix:lint": "eslint -c ./.eslintrc.js src/**/*.ts --fix",
27
27
  "build:clean": "rm dist/ -rf",
28
28
  "build:compile": "tsc -p ./tsconfig.build.json",
29
- "build:complete": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --exclude='**/.behavior/**' --include='**/*.template.md' --include='**/.briefs/**/*.md' --include='**/.briefs/*.md' --include='**/.skills/**/*.sh' --include='**/.skills/*.sh' --exclude='*' src/ dist/",
29
+ "build:complete": "rsync -a --prune-empty-dirs --include='*/' --exclude='**/.route/**' --exclude='**/.scratch/**' --exclude='**/.behavior/**' --exclude='**/*.test.sh' --include='**/*.template.md' --include='**/.briefs/**/*.md' --include='**/.briefs/*.md' --include='**/.skills/**/*.sh' --include='**/.skills/*.sh' --exclude='*' src/ dist/",
30
30
  "build": "npm run build:clean && npm run build:compile && npm run build:complete",
31
31
  "test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
32
32
  "test:types": "tsc -p ./tsconfig.build.json --noEmit",
@@ -1,229 +0,0 @@
1
- #!/usr/bin/env bash
2
- ######################################################################
3
- # .what = test suite for git worktree management scripts
4
- #
5
- # .why = verify all worktree operations work correctly
6
- #
7
- # .how = sets up temp git repo, exercises all commands, cleans up
8
- #
9
- # usage:
10
- # ./git.worktree.test.sh
11
- #
12
- # guarantee:
13
- # - creates isolated temp environment
14
- # - cleans up after itself
15
- # - exits 0 on success, 1 on failure
16
- ######################################################################
17
-
18
- set -uo pipefail
19
-
20
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
- TEST_DIR=""
22
- TESTS_PASSED=0
23
- TESTS_FAILED=0
24
-
25
- # colors for output
26
- RED='\033[0;31m'
27
- GREEN='\033[0;32m'
28
- NC='\033[0m' # No Color
29
-
30
- # test helper: assert condition
31
- assert() {
32
- local description="$1"
33
- local condition="$2"
34
-
35
- if eval "$condition"; then
36
- echo -e " ${GREEN}✓${NC} $description"
37
- ((TESTS_PASSED++))
38
- else
39
- echo -e " ${RED}✗${NC} $description"
40
- ((TESTS_FAILED++))
41
- fi
42
- }
43
-
44
- # test helper: assert output contains
45
- assert_output_contains() {
46
- local description="$1"
47
- local output="$2"
48
- local expected="$3"
49
-
50
- if [[ "$output" == *"$expected"* ]]; then
51
- echo -e " ${GREEN}✓${NC} $description"
52
- ((TESTS_PASSED++))
53
- else
54
- echo -e " ${RED}✗${NC} $description"
55
- echo " expected: $expected"
56
- echo " got: $output"
57
- ((TESTS_FAILED++))
58
- fi
59
- }
60
-
61
- # setup: create temp git repo with remote
62
- setup() {
63
- echo "setting up test environment..."
64
-
65
- # create temp directory
66
- TEST_DIR="$(mktemp -d)"
67
-
68
- # create "remote" bare repo
69
- mkdir -p "$TEST_DIR/remote.git"
70
- git -C "$TEST_DIR/remote.git" init --bare -q
71
-
72
- # create "local" repo
73
- mkdir -p "$TEST_DIR/local"
74
- git -C "$TEST_DIR/local" init -q
75
- git -C "$TEST_DIR/local" config user.email "test@test.com"
76
- git -C "$TEST_DIR/local" config user.name "Test"
77
- git -C "$TEST_DIR/local" remote add origin "$TEST_DIR/remote.git"
78
-
79
- # initial commit
80
- echo "test" > "$TEST_DIR/local/README.md"
81
- git -C "$TEST_DIR/local" add .
82
- git -C "$TEST_DIR/local" commit -m "initial commit" -q
83
-
84
- # push to remote (handle both main and master default branch names)
85
- git -C "$TEST_DIR/local" push -u origin HEAD:main -q 2>/dev/null || true
86
-
87
- echo " test dir: $TEST_DIR"
88
- echo ""
89
- }
90
-
91
- # teardown: clean up temp directory
92
- teardown() {
93
- echo ""
94
- echo "cleaning up..."
95
-
96
- if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
97
- rm -rf "$TEST_DIR"
98
- echo " removed: $TEST_DIR"
99
- fi
100
- }
101
-
102
- # run tests
103
- run_tests() {
104
- local worktree_sh="$SCRIPT_DIR/git.worktree.sh"
105
-
106
- echo "=== test: dispatcher ==="
107
-
108
- # test: dispatcher shows usage
109
- local usage_output
110
- usage_output=$("$worktree_sh" 2>&1 || true)
111
- assert_output_contains "shows usage on no args" "$usage_output" "usage: git.worktree.sh"
112
-
113
- echo ""
114
- echo "=== test: get (empty) ==="
115
-
116
- # test: get with no worktrees
117
- local get_empty
118
- get_empty=$(cd "$TEST_DIR/local" && "$worktree_sh" get)
119
- assert_output_contains "shows no worktrees" "$get_empty" "(no worktrees)"
120
-
121
- echo ""
122
- echo "=== test: set (create) ==="
123
-
124
- # test: set creates worktree
125
- local set_output
126
- set_output=$(cd "$TEST_DIR/local" && "$worktree_sh" set test/branch1)
127
- assert_output_contains "creates worktree" "$set_output" "[CREATE]"
128
- assert_output_contains "correct path" "$set_output" "_worktrees/local/test.branch1"
129
- assert "worktree directory exists" "[[ -d '$TEST_DIR/_worktrees/local/test.branch1' ]]"
130
-
131
- echo ""
132
- echo "=== test: set (idempotent) ==="
133
-
134
- # test: set is idempotent
135
- local set_keep
136
- set_keep=$(cd "$TEST_DIR/local" && "$worktree_sh" set test/branch1)
137
- assert_output_contains "keeps existing worktree" "$set_keep" "[KEEP]"
138
-
139
- echo ""
140
- echo "=== test: get (with worktrees) ==="
141
-
142
- # test: get lists worktrees
143
- local get_list
144
- get_list=$(cd "$TEST_DIR/local" && "$worktree_sh" get)
145
- assert_output_contains "lists worktrees header" "$get_list" "worktrees for local:"
146
- assert_output_contains "lists worktree entry" "$get_list" "test.branch1"
147
-
148
- echo ""
149
- echo "=== test: set (second worktree) ==="
150
-
151
- # test: create second worktree
152
- local set_second
153
- set_second=$(cd "$TEST_DIR/local" && "$worktree_sh" set feature/auth)
154
- assert_output_contains "creates second worktree" "$set_second" "[CREATE]"
155
- assert "second worktree exists" "[[ -d '$TEST_DIR/_worktrees/local/feature.auth' ]]"
156
-
157
- echo ""
158
- echo "=== test: get from worktree ==="
159
-
160
- # test: get from within worktree resolves same dir
161
- local get_from_wt
162
- get_from_wt=$(cd "$TEST_DIR/_worktrees/local/test.branch1" && "$worktree_sh" get)
163
- assert_output_contains "lists from worktree" "$get_from_wt" "worktrees for local:"
164
- assert_output_contains "sees both worktrees" "$get_from_wt" "feature.auth"
165
-
166
- echo ""
167
- echo "=== test: set from worktree ==="
168
-
169
- # test: set from within worktree creates in same _worktrees dir
170
- local set_from_wt
171
- set_from_wt=$(cd "$TEST_DIR/_worktrees/local/test.branch1" && "$worktree_sh" set test/from-wt)
172
- assert_output_contains "creates from worktree" "$set_from_wt" "[CREATE]"
173
- assert "created in same _worktrees dir" "[[ -d '$TEST_DIR/_worktrees/local/test.from-wt' ]]"
174
-
175
- echo ""
176
- echo "=== test: del ==="
177
-
178
- # test: del removes worktree
179
- local del_output
180
- del_output=$(cd "$TEST_DIR/local" && "$worktree_sh" del test/from-wt)
181
- assert_output_contains "deletes worktree" "$del_output" "[DELETE]"
182
- assert "worktree removed" "[[ ! -d '$TEST_DIR/_worktrees/local/test.from-wt' ]]"
183
-
184
- echo ""
185
- echo "=== test: del (skip nonexistent) ==="
186
-
187
- # test: del skips nonexistent
188
- local del_skip
189
- del_skip=$(cd "$TEST_DIR/local" && "$worktree_sh" del nonexistent/branch)
190
- assert_output_contains "skips nonexistent" "$del_skip" "[SKIP]"
191
- assert_output_contains "shows not found" "$del_skip" "(not found)"
192
-
193
- echo ""
194
- echo "=== test: cleanup remaining ==="
195
-
196
- # cleanup remaining test worktrees
197
- cd "$TEST_DIR/local" && "$worktree_sh" del test/branch1 >/dev/null
198
- cd "$TEST_DIR/local" && "$worktree_sh" del feature/auth >/dev/null
199
-
200
- local final_get
201
- final_get=$(cd "$TEST_DIR/local" && "$worktree_sh" get)
202
- assert_output_contains "all cleaned up" "$final_get" "(no worktrees)"
203
- }
204
-
205
- # main
206
- main() {
207
- echo "git.worktree.sh test suite"
208
- echo "=========================="
209
- echo ""
210
-
211
- setup
212
-
213
- # run tests (trap ensures cleanup on failure)
214
- trap teardown EXIT
215
- run_tests
216
-
217
- echo ""
218
- echo "=========================="
219
- echo -e "passed: ${GREEN}$TESTS_PASSED${NC}"
220
- echo -e "failed: ${RED}$TESTS_FAILED${NC}"
221
-
222
- if [[ $TESTS_FAILED -gt 0 ]]; then
223
- exit 1
224
- fi
225
-
226
- exit 0
227
- }
228
-
229
- main "$@"
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env bash
2
- ######################################################################
3
- # # tldr
4
- #
5
- # .what: run tests with proper setup, logs, and context preservation
6
- # .why: preserve test output for review without rerun, auto-configure AWS and testdb
7
- # .how: ./run.test.sh unit
8
- # ./run.test.sh integration "pattern"
9
- # ./run.test.sh acceptance
10
- #
11
- ######################################################################
12
- # # full
13
- #
14
- # .what
15
- #
16
- # run tests with proper setup, logs, and context preservation
17
- #
18
- # supports unit, integration, and acceptance tests with automatic:
19
- # - output logs to .log/test/ for repeated review
20
- # - scope filter and THOROUGH mode
21
- # - AWS profile configuration (for repos with AWS resources)
22
- # - test database provision (when start:testdb is available)
23
- #
24
- #
25
- # .why
26
- #
27
- # tests require different setup depending on their type:
28
- #
29
- # unit tests:
30
- # - isolated, no external dependencies
31
- # - fast execution
32
- # - use --changedSince by default for speed
33
- #
34
- # integration tests:
35
- # - interact with databases and remote resources
36
- # - require AWS credentials (if repo uses AWS)
37
- # - require test databases (if repo uses a testdb)
38
- # - test interactions between components
39
- #
40
- # acceptance tests:
41
- # - end-to-end testing
42
- # - require AWS credentials (if repo uses AWS)
43
- # - require test database provisioning
44
- # - verify complete user workflows
45
- #
46
- # all tests benefit from:
47
- # - output logs via tee to .log/test/{type}/run.{timestamp}.out
48
- # - preserved context for review without rerun
49
- # - comparison of results across test runs
50
- #
51
- #
52
- # .howto.use
53
- #
54
- # ## unit tests
55
- #
56
- # run all unit tests (uses --changedSince for speed):
57
- # ./run.test.sh unit
58
- #
59
- # run unit tests that match a pattern (automatically THOROUGH):
60
- # ./run.test.sh unit "syncPhone"
61
- # ./run.test.sh unit "relate.*Path"
62
- #
63
- # run all unit tests thoroughly (no --changedSince):
64
- # THOROUGH=true ./run.test.sh unit
65
- #
66
- # behavior:
67
- # - no AWS_PROFILE configuration
68
- # - no test database provision
69
- # - uses --changedSince by default (unless scope provided or THOROUGH=true)
70
- # - logs to .log/test/unit/run.{timestamp}.out
71
- #
72
- #
73
- # ## integration tests
74
- #
75
- # run all integration tests:
76
- # ./run.test.sh integration
77
- #
78
- # run integration tests that match a pattern:
79
- # ./run.test.sh integration "database.*sync"
80
- # ./run.test.sh integration "whodis"
81
- #
82
- # behavior:
83
- # - sets AWS_PROFILE=$org.dev (if awsAccountId in declapract.use.yml)
84
- # - runs start:testdb (if available in package.json)
85
- # - uses --changedSince by default (unless scope provided or THOROUGH=true)
86
- # - logs to .log/test/integration/run.{timestamp}.out
87
- #
88
- #
89
- # ## acceptance tests
90
- #
91
- # run all acceptance tests locally:
92
- # ./run.test.sh acceptance
93
- #
94
- # run acceptance tests that match a pattern:
95
- # ./run.test.sh acceptance "user.*flow"
96
- #
97
- # behavior:
98
- # - sets AWS_PROFILE=$org.dev (if awsAccountId in declapract.use.yml)
99
- # - runs start:testdb (if available in package.json)
100
- # - uses test:acceptance:locally (local execution only for now)
101
- # - logs to .log/test/acceptance/run.{timestamp}.out
102
- #
103
- #
104
- # ## review test output
105
- #
106
- # all test output is logged via tee to .log/test/{type}/run.{timestamp}.out
107
- #
108
- # review the latest test run:
109
- # cat .log/test/unit/run.*.out | tail -n 1 | xargs cat
110
- #
111
- # compare test results across runs:
112
- # ls -t .log/test/unit/
113
- # diff .log/test/unit/run.2025-11-23T15-00-00Z.out \
114
- # .log/test/unit/run.2025-11-23T15-10-00Z.out
115
- #
116
- # search for failures in logs:
117
- # grep -r "FAIL" .log/test/unit/
118
- #
119
- #
120
- # .guarantee
121
- #
122
- # ✔ configure AWS_PROFILE only if awsAccountId in declapract.use.yml
123
- # ✔ provision test database only if start:testdb in package.json
124
- # ✔ log output to .log/test/{type}/run.{timestamp}.out via tee
125
- # ✔ preserve context for repeated review without rerun
126
- # ✔ support jest pattern/scope filter
127
- # ✔ automatically set THOROUGH=true when scope is provided
128
- # ✔ fail-fast on test failures
129
- # ✔ show relative paths for easy navigation
130
- ######################################################################
131
-
132
- set -euo pipefail
133
-
134
- # Parse arguments
135
- TEST_TYPE="${1:-}"
136
- SCOPE="${2:-}"
137
-
138
- # Default to THOROUGH mode when scope is provided (unless explicitly set)
139
- if [[ -n "$SCOPE" ]] && [[ -z "${THOROUGH:-}" ]]; then
140
- export THOROUGH=true
141
- fi
142
-
143
- # Validate test type
144
- if [[ -z "$TEST_TYPE" ]]; then
145
- echo "✗ test type required"
146
- echo ""
147
- echo "usage: $0 <type> [pattern]"
148
- echo ""
149
- echo "types:"
150
- echo " unit - run unit tests"
151
- echo " integration - run integration tests"
152
- echo " acceptance - run acceptance tests"
153
- echo ""
154
- echo "examples:"
155
- echo " $0 unit"
156
- echo " $0 integration 'database.*sync'"
157
- echo " THOROUGH=true $0 unit"
158
- exit 1
159
- fi
160
-
161
- if [[ ! "$TEST_TYPE" =~ ^(unit|integration|acceptance)$ ]]; then
162
- echo "✗ invalid test type: $TEST_TYPE"
163
- echo " valid types: unit, integration, acceptance"
164
- exit 1
165
- fi
166
-
167
- PROJECT_ROOT="$PWD"
168
- LOG_DIR="$PROJECT_ROOT/.log/test/$TEST_TYPE"
169
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H-%M-%SZ")
170
- LOG_FILE="$LOG_DIR/run.$TIMESTAMP.out"
171
-
172
- # Ensure log directory exists
173
- mkdir -p "$LOG_DIR"
174
-
175
- echo "→ run $TEST_TYPE tests"
176
- echo "→ log to: ${LOG_FILE#$PROJECT_ROOT/}"
177
- echo ""
178
-
179
- # Configure AWS profile and provision test database for integration/acceptance tests
180
- if [[ "$TEST_TYPE" == "integration" ]] || [[ "$TEST_TYPE" == "acceptance" ]]; then
181
- # Check if awsAccountId is specified in declapract.use.yml
182
- if [[ -f "declapract.use.yml" ]] && grep -q "awsAccountId:" declapract.use.yml; then
183
- # Extract organization from declapract.use.yml
184
- ORGANIZATION=$(grep -E '^\s*organizationName:' declapract.use.yml | sed "s/.*organizationName:[[:space:]]*['\"]*//" | sed "s/['\"].*//")
185
-
186
- if [[ -n "$ORGANIZATION" ]]; then
187
- # Configure AWS profile for dev resources
188
- export AWS_PROFILE="$ORGANIZATION.dev"
189
- echo "→ AWS_PROFILE=$AWS_PROFILE"
190
- fi
191
- fi
192
-
193
- # Start test database if available in package.json
194
- if npm run | grep -q "start:testdb"; then
195
- echo "→ start:testdb"
196
- echo ""
197
- npm run start:testdb 2>&1 | tee -a "$LOG_FILE"
198
- fi
199
- fi
200
-
201
- # Build the test command
202
- case "$TEST_TYPE" in
203
- unit)
204
- TEST_COMMAND="npm run test:unit"
205
- ;;
206
- integration)
207
- TEST_COMMAND="npm run test:integration"
208
- ;;
209
- acceptance)
210
- # only support local acceptance tests for now
211
- TEST_COMMAND="npm run test:acceptance:locally"
212
- ;;
213
- esac
214
-
215
- # Add scope filter if provided
216
- if [[ -n "$SCOPE" ]]; then
217
- echo "→ scope filter: $SCOPE"
218
- echo ""
219
- TEST_COMMAND="$TEST_COMMAND -- '$SCOPE'"
220
- else
221
- echo "→ scope: all tests"
222
- echo ""
223
- fi
224
-
225
- # Run tests with output logged via tee
226
- echo "> $TEST_COMMAND" | tee -a "$LOG_FILE"
227
- echo "" | tee -a "$LOG_FILE"
228
-
229
- # For unit tests, strip color codes from log file while preserving them in terminal output
230
- if [[ "$TEST_TYPE" == "unit" ]]; then
231
- eval "$TEST_COMMAND" 2>&1 | tee >(sed 's/\x1B\[[0-9;]*[JKmsu]//g' >> "$LOG_FILE")
232
- TEST_EXIT_CODE=${PIPESTATUS[0]}
233
- else
234
- eval "$TEST_COMMAND" 2>&1 | tee -a "$LOG_FILE"
235
- TEST_EXIT_CODE=${PIPESTATUS[0]}
236
- fi
237
-
238
- echo "" | tee -a "$LOG_FILE"
239
-
240
- if [[ $TEST_EXIT_CODE -eq 0 ]]; then
241
- echo "✓ $TEST_TYPE tests complete!" | tee -a "$LOG_FILE"
242
- echo "→ log saved: ${LOG_FILE#$PROJECT_ROOT/}"
243
- exit 0
244
- else
245
- echo "✗ $TEST_TYPE tests failed" | tee -a "$LOG_FILE"
246
- echo "→ log saved: ${LOG_FILE#$PROJECT_ROOT/}"
247
- echo ""
248
- echo "→ review the log file for details:"
249
- echo " cat ${LOG_FILE#$PROJECT_ROOT/}"
250
- exit $TEST_EXIT_CODE
251
- fi