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.
- package/dist/logic/roles/mechanic/.briefs/patterns/code.prod.repo.structure/bad-practices/forbid.barrel.exports.ts.md +41 -0
- package/dist/logic/roles/mechanic/.briefs/patterns/lang.terms/bad-practices/forbid.term=existing.md +10 -0
- package/dist/logic/roles/mechanic/.skills/claude.hooks/check.pretooluse.permissions.sh +193 -0
- package/dist/logic/roles/mechanic/.skills/git.worktree.common.sh +58 -0
- package/dist/logic/roles/mechanic/.skills/git.worktree.del.sh +51 -0
- package/dist/logic/roles/mechanic/.skills/git.worktree.get.sh +51 -0
- package/dist/logic/roles/mechanic/.skills/git.worktree.set.sh +108 -0
- package/dist/logic/roles/mechanic/.skills/git.worktree.sh +46 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.pretooluse.sh +118 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sessionstart.sh +113 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sh +11 -100
- package/dist/logic/roles/mechanic/.skills/init.claude.permissions.sh +32 -7
- package/package.json +2 -2
- package/dist/logic/roles/mechanic/.skills/run.test.sh +0 -251
|
@@ -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,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
|
|
3
|
+
# .what = bind all mechanic hooks to Claude settings
|
|
4
4
|
#
|
|
5
|
-
# .why = the mechanic role
|
|
6
|
-
#
|
|
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
|
|
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 =
|
|
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
|
-
# ✔
|
|
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
|
-
|
|
27
|
-
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
20
|
+
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
21
|
|
|
29
|
-
#
|
|
30
|
-
|
|
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
|
-
|
|
51
|
-
"Bash(
|
|
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.
|
|
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
|