rhachet-roles-ehmpathy 1.10.0 → 1.12.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,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,193 @@
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
+ # Convert glob * to regex .*
116
+ local regex="^${pattern//\*/.*}$"
117
+
118
+ if [[ "$cmd" =~ $regex ]]; then
119
+ return 0
120
+ fi
121
+ return 1
122
+ }
123
+
124
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
125
+ if match_pattern "$COMMAND" "$pattern"; then
126
+ exit 0 # Command matches an allowed pattern
127
+ fi
128
+ done
129
+
130
+ # Command not matched - handle based on mode
131
+
132
+ # SOFTNUDGE mode: provide guidance but don't block (early return)
133
+ # Output plain text - no hookSpecificOutput so normal permission flow continues
134
+ if [[ "$MODE" == "SOFTNUDGE" ]]; then
135
+ echo ""
136
+ echo "⚠️ This command is not covered by existing pre-approved permissions."
137
+ echo ""
138
+ echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
139
+ echo ""
140
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
141
+ echo " • $pattern"
142
+ done
143
+ echo ""
144
+ echo "If an existing permission pattern can solve your task, use that instead."
145
+ echo "If not, proceed with requesting approval."
146
+ echo ""
147
+ exit 0
148
+ fi
149
+
150
+ # HARDNUDGE mode (default): block on first attempt, allow on retry
151
+ CLAUDE_DIR=$(find_claude_dir) || {
152
+ echo "ERROR: No .claude directory found. This hook requires a .claude directory." >&2
153
+ exit 1
154
+ }
155
+ ATTEMPTED_FILE="$CLAUDE_DIR/permission.nudges.local.json"
156
+
157
+ # Ensure the file exists with valid JSON
158
+ if [[ ! -f "$ATTEMPTED_FILE" ]]; then
159
+ echo '{}' > "$ATTEMPTED_FILE"
160
+ fi
161
+
162
+ # Check if this command was recently attempted
163
+ now=$(date +%s)
164
+ last_attempt=$(jq -r --arg cmd "$COMMAND" '.[$cmd] // 0' "$ATTEMPTED_FILE" 2>/dev/null || echo "0")
165
+ elapsed=$((now - last_attempt))
166
+
167
+ if [[ $elapsed -lt $HARDNUDGE_WINDOW_SECONDS ]]; then
168
+ # Claude already tried within the window - they've thought twice
169
+ # Exit silently with 0 so normal permission flow continues (user gets prompted)
170
+ exit 0
171
+ fi
172
+
173
+ # First attempt - record timestamp and block
174
+ # Use a temp file for atomic update
175
+ tmp_file=$(mktemp)
176
+ jq --arg cmd "$COMMAND" --argjson ts "$now" '. + {($cmd): $ts}' "$ATTEMPTED_FILE" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$ATTEMPTED_FILE"
177
+
178
+ # Output block message to stderr and exit 2 to deny
179
+ {
180
+ echo ""
181
+ echo "🛑 BLOCKED: This command is not covered by existing pre-approved permissions."
182
+ echo ""
183
+ echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
184
+ echo ""
185
+ for pattern in "${ALLOWED_PATTERNS[@]}"; do
186
+ echo " • $pattern"
187
+ done
188
+ echo ""
189
+ echo "If an existing permission pattern can solve your task, use that instead."
190
+ echo "If you've considered the alternatives and still need this specific command, retry it."
191
+ echo ""
192
+ } >&2
193
+ exit 2 # Exit 2 = block with error message
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = shared helpers for git worktree management
4
+ #
5
+ # .why = centralizes path resolution and branch sanitization logic
6
+ # used by git.worktree.{get,set,del}.sh scripts
7
+ #
8
+ # .how = source this file to get access to:
9
+ # - resolve_worktrees_dir: computes $REPO_WORKTREES_DIR
10
+ # - sanitize_branch_name: converts branch to worktree name
11
+ # - get_repo_name: extracts repo name from gitroot
12
+ #
13
+ # guarantee:
14
+ # - works from root repo or from within a worktree
15
+ # - consistent path resolution across all worktree scripts
16
+ ######################################################################
17
+
18
+ # resolve the worktrees directory for this repo
19
+ # handles both root repo and worktree contexts
20
+ resolve_worktrees_dir() {
21
+ local gitroot
22
+ gitroot="$(git rev-parse --show-toplevel)"
23
+
24
+ local reponame
25
+ reponame="$(basename "$gitroot")"
26
+
27
+ # detect if we're in a worktree (path contains _worktrees)
28
+ if [[ "$gitroot" == *"_worktrees"* ]]; then
29
+ # we're in a worktree - reuse same _worktrees/$reponame dir
30
+ echo "${gitroot%/*}"
31
+ else
32
+ # root repo - compute sibling _worktrees dir
33
+ echo "$(dirname "$gitroot")/_worktrees/$reponame"
34
+ fi
35
+ }
36
+
37
+ # sanitize branch name for use as directory name
38
+ # vlad/practs => vlad.practs
39
+ sanitize_branch_name() {
40
+ local branch="$1"
41
+ echo "${branch//\//.}"
42
+ }
43
+
44
+ # get the repo name (works from root repo or worktree)
45
+ get_repo_name() {
46
+ local gitroot
47
+ gitroot="$(git rev-parse --show-toplevel)"
48
+
49
+ # detect if we're in a worktree (path contains _worktrees)
50
+ if [[ "$gitroot" == *"_worktrees"* ]]; then
51
+ # extract repo name from _worktrees/$reponame/$worktree path
52
+ # gitroot = /path/to/_worktrees/$reponame/$worktree
53
+ local worktrees_parent="${gitroot%/*}" # /path/to/_worktrees/$reponame
54
+ basename "$worktrees_parent"
55
+ else
56
+ basename "$gitroot"
57
+ fi
58
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = remove a git worktree
4
+ #
5
+ # .why = clean up worktrees no longer needed
6
+ #
7
+ # .how = removes worktree at @gitroot/../_worktrees/$reponame/$branch
8
+ #
9
+ # usage:
10
+ # git.worktree.del.sh <branch>
11
+ #
12
+ # guarantee:
13
+ # - idempotent: [DELETE] if exists, [SKIP] if not found
14
+ # - works from root repo or from within a worktree
15
+ ######################################################################
16
+
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+
21
+ # source shared helpers
22
+ source "$SCRIPT_DIR/git.worktree.common.sh"
23
+
24
+ # parse arguments
25
+ BRANCH="${1:-}"
26
+
27
+ # validate branch argument
28
+ if [[ -z "$BRANCH" ]]; then
29
+ echo "error: branch name required"
30
+ echo "usage: git.worktree.del.sh <branch>"
31
+ exit 1
32
+ fi
33
+
34
+ # resolve paths
35
+ REPO_WORKTREES_DIR="$(resolve_worktrees_dir)"
36
+ WORKTREE_NAME="$(sanitize_branch_name "$BRANCH")"
37
+ WORKTREE_PATH="$REPO_WORKTREES_DIR/$WORKTREE_NAME"
38
+
39
+ # delete logic
40
+ if [[ -d "$WORKTREE_PATH" ]]; then
41
+ # remove worktree via git
42
+ git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || {
43
+ # fallback: manual removal if git worktree remove fails
44
+ rm -rf "$WORKTREE_PATH"
45
+ git worktree prune
46
+ }
47
+
48
+ echo "[DELETE] $WORKTREE_NAME"
49
+ else
50
+ echo "[SKIP] $WORKTREE_NAME (not found)"
51
+ fi
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = list git worktrees for this repo
4
+ #
5
+ # .why = discover existing worktrees managed by git.worktree.sh
6
+ #
7
+ # .how = lists worktrees at @gitroot/../_worktrees/$reponame/
8
+ #
9
+ # usage:
10
+ # git.worktree.get.sh
11
+ #
12
+ # guarantee:
13
+ # - works from root repo or from within a worktree
14
+ # - shows "(no worktrees)" if none exist
15
+ ######################################################################
16
+
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+
21
+ # source shared helpers
22
+ source "$SCRIPT_DIR/git.worktree.common.sh"
23
+
24
+ # resolve worktrees directory
25
+ REPO_WORKTREES_DIR="$(resolve_worktrees_dir)"
26
+ REPO_NAME="$(get_repo_name)"
27
+
28
+ # check if worktrees directory exists
29
+ if [[ ! -d "$REPO_WORKTREES_DIR" ]]; then
30
+ echo "(no worktrees)"
31
+ exit 0
32
+ fi
33
+
34
+ # list worktrees
35
+ WORKTREES=()
36
+ for dir in "$REPO_WORKTREES_DIR"/*/; do
37
+ [[ -d "$dir" ]] && WORKTREES+=("$dir")
38
+ done
39
+
40
+ # handle empty
41
+ if [[ ${#WORKTREES[@]} -eq 0 ]]; then
42
+ echo "(no worktrees)"
43
+ exit 0
44
+ fi
45
+
46
+ # output worktree list
47
+ echo "worktrees for $REPO_NAME:"
48
+ for dir in "${WORKTREES[@]}"; do
49
+ name="$(basename "$dir")"
50
+ echo " $name => $dir"
51
+ done
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = findsert a git worktree for a branch
4
+ #
5
+ # .why = enable parallel work on same repo without nested worktrees
6
+ # worktrees are placed outside repo so git diff stays clean
7
+ #
8
+ # .how = creates worktree at @gitroot/../_worktrees/$reponame/$branch
9
+ #
10
+ # usage:
11
+ # git.worktree.set.sh <branch> # findsert worktree
12
+ # git.worktree.set.sh <branch> --open # findsert + open in codium
13
+ # git.worktree.set.sh <branch> --main # create from origin/main
14
+ #
15
+ # guarantee:
16
+ # - idempotent: [KEEP] if exists, [CREATE] if not
17
+ # - works from root repo or from within a worktree
18
+ # - worktree placed outside repo (not nested)
19
+ ######################################################################
20
+
21
+ set -euo pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+
25
+ # source shared helpers
26
+ source "$SCRIPT_DIR/git.worktree.common.sh"
27
+
28
+ # parse arguments
29
+ BRANCH=""
30
+ FLAG_OPEN=false
31
+ FLAG_MAIN=false
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case $1 in
35
+ --open)
36
+ FLAG_OPEN=true
37
+ shift
38
+ ;;
39
+ --main)
40
+ FLAG_MAIN=true
41
+ shift
42
+ ;;
43
+ -*)
44
+ echo "error: unknown flag '$1'"
45
+ echo "usage: git.worktree.set.sh <branch> [--open] [--main]"
46
+ exit 1
47
+ ;;
48
+ *)
49
+ if [[ -z "$BRANCH" ]]; then
50
+ BRANCH="$1"
51
+ else
52
+ echo "error: unexpected argument '$1'"
53
+ exit 1
54
+ fi
55
+ shift
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ # validate branch argument
61
+ if [[ -z "$BRANCH" ]]; then
62
+ echo "error: branch name required"
63
+ echo "usage: git.worktree.set.sh <branch> [--open] [--main]"
64
+ exit 1
65
+ fi
66
+
67
+ # resolve paths
68
+ REPO_WORKTREES_DIR="$(resolve_worktrees_dir)"
69
+ WORKTREE_NAME="$(sanitize_branch_name "$BRANCH")"
70
+ WORKTREE_PATH="$REPO_WORKTREES_DIR/$WORKTREE_NAME"
71
+
72
+ # ensure parent directory exists
73
+ mkdir -p "$REPO_WORKTREES_DIR"
74
+
75
+ # findsert logic
76
+ if [[ -d "$WORKTREE_PATH" ]]; then
77
+ echo "[KEEP] $WORKTREE_NAME => $WORKTREE_PATH"
78
+ else
79
+ # create worktree
80
+ if [[ "$FLAG_MAIN" == true ]]; then
81
+ # fetch latest main first
82
+ git fetch origin main 2>/dev/null || git fetch origin master 2>/dev/null || true
83
+
84
+ # create new branch from origin/main
85
+ git worktree add -b "$BRANCH" "$WORKTREE_PATH" origin/main 2>/dev/null || \
86
+ git worktree add -b "$BRANCH" "$WORKTREE_PATH" origin/master
87
+ else
88
+ # check if branch exists
89
+ if git show-ref --verify --quiet "refs/heads/$BRANCH" 2>/dev/null; then
90
+ # branch exists locally, checkout existing
91
+ git worktree add "$WORKTREE_PATH" "$BRANCH"
92
+ elif git show-ref --verify --quiet "refs/remotes/origin/$BRANCH" 2>/dev/null; then
93
+ # branch exists on remote, track it
94
+ git worktree add --track -b "$BRANCH" "$WORKTREE_PATH" "origin/$BRANCH"
95
+ else
96
+ # create new branch from current HEAD
97
+ git worktree add -b "$BRANCH" "$WORKTREE_PATH"
98
+ fi
99
+ fi
100
+
101
+ echo "[CREATE] $WORKTREE_NAME => $WORKTREE_PATH"
102
+ fi
103
+
104
+ # open in codium if requested
105
+ if [[ "$FLAG_OPEN" == true ]]; then
106
+ echo "opening in codium..."
107
+ codium "$WORKTREE_PATH"
108
+ fi
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = dispatcher for git worktree management
4
+ #
5
+ # .why = single entry point for get|set|del subcommands
6
+ # enables convenient `git.worktree.sh get|set|del` interface
7
+ #
8
+ # .how = routes to git.worktree.{get,set,del}.sh based on subcommand
9
+ #
10
+ # usage:
11
+ # git.worktree.sh get # list worktrees
12
+ # git.worktree.sh set <branch> # findsert worktree
13
+ # git.worktree.sh set <branch> --open # findsert + open in codium
14
+ # git.worktree.sh set <branch> --main # create from origin/main
15
+ # git.worktree.sh del <branch> # remove worktree
16
+ #
17
+ # guarantee:
18
+ # - dispatches to correct subcommand script
19
+ # - shows usage on invalid/missing subcommand
20
+ # - fail-fast on errors
21
+ ######################################################################
22
+
23
+ set -euo pipefail
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ SUBCOMMAND="${1:-}"
27
+
28
+ case "$SUBCOMMAND" in
29
+ get|set|del)
30
+ shift
31
+ exec "$SCRIPT_DIR/git.worktree.$SUBCOMMAND.sh" "$@"
32
+ ;;
33
+ *)
34
+ echo "usage: git.worktree.sh <command> [args]"
35
+ echo ""
36
+ echo "commands:"
37
+ echo " get list worktrees for this repo"
38
+ echo " set <branch> findsert worktree for branch"
39
+ echo " del <branch> remove worktree for branch"
40
+ echo ""
41
+ echo "options (for set):"
42
+ echo " --open open worktree in codium after creation"
43
+ echo " --main create branch from origin/main"
44
+ exit 1
45
+ ;;
46
+ esac
@@ -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:*)",
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.10.0",
5
+ "version": "1.12.0",
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,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