sequant 1.16.1 → 1.18.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +14 -2
- package/README.md +2 -0
- package/dist/bin/cli.js +2 -1
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
- package/dist/marketplace/external_plugins/sequant/README.md +38 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
- package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
- package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
- package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
- package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
- package/dist/src/commands/run.d.ts +13 -274
- package/dist/src/commands/run.js +43 -1958
- package/dist/src/commands/sync.js +3 -0
- package/dist/src/commands/update.js +3 -0
- package/dist/src/lib/plugin-version-sync.d.ts +2 -1
- package/dist/src/lib/plugin-version-sync.js +28 -7
- package/dist/src/lib/solve-comment-parser.d.ts +26 -0
- package/dist/src/lib/solve-comment-parser.js +63 -7
- package/dist/src/lib/upstream/assessment.js +6 -3
- package/dist/src/lib/upstream/relevance.d.ts +5 -0
- package/dist/src/lib/upstream/relevance.js +24 -0
- package/dist/src/lib/upstream/report.js +18 -46
- package/dist/src/lib/upstream/types.d.ts +2 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
- package/dist/src/lib/workflow/batch-executor.js +574 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
- package/dist/src/lib/workflow/phase-executor.js +381 -0
- package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
- package/dist/src/lib/workflow/phase-mapper.js +147 -0
- package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
- package/dist/src/lib/workflow/pr-operations.js +326 -0
- package/dist/src/lib/workflow/pr-status.d.ts +49 -0
- package/dist/src/lib/workflow/pr-status.js +131 -0
- package/dist/src/lib/workflow/run-reflect.d.ts +32 -0
- package/dist/src/lib/workflow/run-reflect.js +191 -0
- package/dist/src/lib/workflow/run-summary.d.ts +36 -0
- package/dist/src/lib/workflow/run-summary.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +79 -0
- package/dist/src/lib/workflow/state-cleanup.js +250 -0
- package/dist/src/lib/workflow/state-rebuild.d.ts +38 -0
- package/dist/src/lib/workflow/state-rebuild.js +140 -0
- package/dist/src/lib/workflow/state-utils.d.ts +14 -162
- package/dist/src/lib/workflow/state-utils.js +10 -677
- package/dist/src/lib/workflow/worktree-discovery.d.ts +61 -0
- package/dist/src/lib/workflow/worktree-discovery.js +229 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
- package/dist/src/lib/workflow/worktree-manager.js +918 -0
- package/package.json +4 -2
- package/templates/skills/exec/SKILL.md +2 -2
- package/templates/skills/fullsolve/SKILL.md +15 -5
- package/templates/skills/loop/SKILL.md +1 -1
- package/templates/skills/qa/SKILL.md +47 -7
- package/templates/skills/solve/SKILL.md +92 -6
- package/templates/skills/spec/SKILL.md +57 -4
- package/templates/skills/test/SKILL.md +10 -0
- package/templates/skills/testgen/SKILL.md +1 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-tool hook for Claude Code
|
|
3
|
+
# - Security guardrails (blocks catastrophic commands)
|
|
4
|
+
# - Timing instrumentation for performance analysis
|
|
5
|
+
# Exit 0 = allow, Exit 2 = block (Exit 1 = non-blocking error, logged but not blocked)
|
|
6
|
+
|
|
7
|
+
# === ROLLBACK MECHANISM ===
|
|
8
|
+
# Set CLAUDE_HOOKS_DISABLED=true to bypass all hook logic
|
|
9
|
+
if [[ "${CLAUDE_HOOKS_DISABLED:-}" == "true" ]]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# === READ INPUT FROM STDIN ===
|
|
14
|
+
# Claude Code passes tool data as JSON via stdin, not environment variables
|
|
15
|
+
INPUT_JSON=$(cat)
|
|
16
|
+
|
|
17
|
+
# Parse JSON using jq (preferred) or fallback to grep
|
|
18
|
+
if command -v jq &>/dev/null; then
|
|
19
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')
|
|
20
|
+
# For Bash tool, extract .command from tool_input; for others, stringify the whole object
|
|
21
|
+
if [[ "$(echo "$INPUT_JSON" | jq -r '.tool_name // empty')" == "Bash" ]]; then
|
|
22
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input.command // empty')
|
|
23
|
+
else
|
|
24
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | jq -r '.tool_input | tostring // empty')
|
|
25
|
+
fi
|
|
26
|
+
else
|
|
27
|
+
TOOL_NAME=$(echo "$INPUT_JSON" | grep -oE '"tool_name"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
28
|
+
# For Bash tool, extract command from tool_input; for others, extract the whole object
|
|
29
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
30
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
31
|
+
else
|
|
32
|
+
TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_input"\s*:\s*\{[^}]+\}' | head -1)
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
TIMING_LOG="/tmp/claude-timing.log"
|
|
37
|
+
PARALLEL_MARKER_PREFIX="/tmp/claude-parallel-"
|
|
38
|
+
|
|
39
|
+
# === AGENT ID DETECTION ===
|
|
40
|
+
# For parallel agents, detect group ID from marker files
|
|
41
|
+
# Format: /tmp/claude-parallel-<group-id>.marker
|
|
42
|
+
AGENT_ID=""
|
|
43
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
44
|
+
if [[ -f "$marker" ]]; then
|
|
45
|
+
# Extract group ID from marker filename
|
|
46
|
+
AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
|
|
47
|
+
break
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# === TIMING START ===
|
|
52
|
+
# Include agent ID in log format if available (AC-4)
|
|
53
|
+
if [[ -n "$AGENT_ID" ]]; then
|
|
54
|
+
echo "$(date +%s.%N) [$AGENT_ID] START $TOOL_NAME" >> "$TIMING_LOG"
|
|
55
|
+
else
|
|
56
|
+
echo "$(date +%s.%N) START $TOOL_NAME" >> "$TIMING_LOG"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# === LOG ROTATION ===
|
|
60
|
+
# Rotate if over 1000 lines to prevent unbounded growth
|
|
61
|
+
if [[ -f "$TIMING_LOG" ]]; then
|
|
62
|
+
LINE_COUNT=$(wc -l < "$TIMING_LOG" 2>/dev/null || echo 0)
|
|
63
|
+
if [[ "$LINE_COUNT" -gt 1000 ]]; then
|
|
64
|
+
tail -500 "$TIMING_LOG" > "${TIMING_LOG}.tmp" && mv "${TIMING_LOG}.tmp" "$TIMING_LOG"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# === CATASTROPHIC BLOCKS ===
|
|
69
|
+
# These should NEVER run in any automated context
|
|
70
|
+
# Only check Bash commands — Write/Edit content may contain these as config strings
|
|
71
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
72
|
+
|
|
73
|
+
# Secrets/credentials
|
|
74
|
+
# Skip check for gh commands (comment/pr bodies may contain example text)
|
|
75
|
+
if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) '; then
|
|
76
|
+
# Pattern requires command to START with file reader (not match in quoted strings)
|
|
77
|
+
if echo "$TOOL_INPUT" | grep -qE '^(cat|less|head|tail|more) .*\.(env|pem|key)'; then
|
|
78
|
+
echo "HOOK_BLOCKED: Reading secret file" | tee -a /tmp/claude-hook.log >&2
|
|
79
|
+
exit 2
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if echo "$TOOL_INPUT" | grep -qE '^(cat|less) .*~/\.(ssh|aws|gnupg|config/gh)'; then
|
|
83
|
+
echo "HOOK_BLOCKED: Reading credential directory" | tee -a /tmp/claude-hook.log >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Bare environment dump
|
|
89
|
+
if echo "$TOOL_INPUT" | grep -qE '^(env|printenv|export)$'; then
|
|
90
|
+
echo "HOOK_BLOCKED: Environment dump" | tee -a /tmp/claude-hook.log >&2
|
|
91
|
+
exit 2
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Destructive system commands
|
|
95
|
+
if echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
|
|
96
|
+
echo "HOOK_BLOCKED: Destructive system command" | tee -a /tmp/claude-hook.log >&2
|
|
97
|
+
exit 2
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Deployment (should never happen in issue automation)
|
|
101
|
+
if echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
|
|
102
|
+
echo "HOOK_BLOCKED: Deployment command" | tee -a /tmp/claude-hook.log >&2
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Force push
|
|
107
|
+
# Pattern requires -f to be a standalone flag (not part of branch name like -fix)
|
|
108
|
+
if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
|
|
109
|
+
echo "HOOK_BLOCKED: Force push" | tee -a /tmp/claude-hook.log >&2
|
|
110
|
+
exit 2
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --- Hard Reset Protection (Issue #85, enhanced) ---
|
|
114
|
+
# Block git reset --hard when there is local work that would be lost:
|
|
115
|
+
# - Unpushed commits on main/master
|
|
116
|
+
# - Uncommitted changes (staged or unstaged)
|
|
117
|
+
# - Unfinished merge in progress
|
|
118
|
+
if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
|
|
119
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
120
|
+
BLOCK_REASONS=""
|
|
121
|
+
|
|
122
|
+
# Check 1: Unpushed commits (only on main/master)
|
|
123
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
124
|
+
UNPUSHED=$(git log origin/$CURRENT_BRANCH..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
125
|
+
if [[ "$UNPUSHED" -gt 0 ]]; then
|
|
126
|
+
BLOCK_REASONS="${BLOCK_REASONS} - $UNPUSHED unpushed commit(s) on $CURRENT_BRANCH\n"
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Check 2: Uncommitted changes (staged or unstaged)
|
|
131
|
+
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
132
|
+
if [[ "$UNCOMMITTED" -gt 0 ]]; then
|
|
133
|
+
BLOCK_REASONS="${BLOCK_REASONS} - $UNCOMMITTED uncommitted file(s)\n"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Check 3: Unfinished merge
|
|
137
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo ".git")
|
|
138
|
+
if [[ -f "$GIT_DIR/MERGE_HEAD" ]]; then
|
|
139
|
+
BLOCK_REASONS="${BLOCK_REASONS} - Unfinished merge in progress\n"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Block if any reasons found
|
|
143
|
+
if [[ -n "$BLOCK_REASONS" ]]; then
|
|
144
|
+
{
|
|
145
|
+
echo "HOOK_BLOCKED: git reset --hard would lose local work:"
|
|
146
|
+
echo -e "$BLOCK_REASONS"
|
|
147
|
+
echo " Resolve with:"
|
|
148
|
+
echo " git push origin $CURRENT_BRANCH # push commits"
|
|
149
|
+
echo " git stash # save changes"
|
|
150
|
+
echo " git merge --abort # cancel merge"
|
|
151
|
+
echo " Or run directly in terminal (outside Claude Code) to bypass"
|
|
152
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
153
|
+
exit 2
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
158
|
+
if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
|
|
159
|
+
echo "HOOK_BLOCKED: Workflow trigger" | tee -a /tmp/claude-hook.log >&2
|
|
160
|
+
exit 2
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
fi # end TOOL_NAME == "Bash" guard for catastrophic blocks
|
|
164
|
+
|
|
165
|
+
# === SECURITY GUARDRAILS ===
|
|
166
|
+
# Granular disable: Set CLAUDE_HOOKS_SECURITY=false to bypass security checks only
|
|
167
|
+
# (separate from CLAUDE_HOOKS_DISABLED which bypasses ALL hooks)
|
|
168
|
+
|
|
169
|
+
# --- Secret Detection (AC-1 for Issue #492) ---
|
|
170
|
+
# Block commits containing hardcoded API keys, tokens, and secrets
|
|
171
|
+
check_secrets() {
|
|
172
|
+
local content="$1"
|
|
173
|
+
local patterns=(
|
|
174
|
+
'sk-[a-zA-Z0-9]{32,}' # OpenAI API key
|
|
175
|
+
'sk_live_[a-zA-Z0-9]{24,}' # Stripe live key
|
|
176
|
+
'AKIA[A-Z0-9]{16}' # AWS Access Key
|
|
177
|
+
'ghp_[a-zA-Z0-9]{36}' # GitHub Personal Token
|
|
178
|
+
'xoxb-[0-9]{10,}(-[a-zA-Z0-9]+)+' # Slack Bot Token
|
|
179
|
+
'AIza[a-zA-Z0-9_-]{35}' # Google API Key
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for pattern in "${patterns[@]}"; do
|
|
183
|
+
if echo "$content" | grep -qE "$pattern"; then
|
|
184
|
+
return 0 # Found a secret
|
|
185
|
+
fi
|
|
186
|
+
done
|
|
187
|
+
return 1 # No secrets found
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# --- Sensitive File Detection (AC-2 for Issue #492) ---
|
|
191
|
+
# Block commits containing sensitive files
|
|
192
|
+
check_sensitive_files() {
|
|
193
|
+
local files="$1"
|
|
194
|
+
local patterns=(
|
|
195
|
+
'\.env$'
|
|
196
|
+
'\.env\.local$'
|
|
197
|
+
'\.env\.production$'
|
|
198
|
+
'\.env\.[^.]+$' # Any .env.* file
|
|
199
|
+
'credentials\.json$'
|
|
200
|
+
'\.pem$'
|
|
201
|
+
'\.key$'
|
|
202
|
+
'id_rsa$'
|
|
203
|
+
'id_ed25519$'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
for pattern in "${patterns[@]}"; do
|
|
207
|
+
if echo "$files" | grep -qE "$pattern"; then
|
|
208
|
+
return 0 # Found sensitive file
|
|
209
|
+
fi
|
|
210
|
+
done
|
|
211
|
+
return 1 # No sensitive files found
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
|
|
215
|
+
# Security checks for git commit
|
|
216
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
217
|
+
# Skip security checks if --no-verify is used
|
|
218
|
+
if ! echo "$TOOL_INPUT" | grep -qE -- '--no-verify'; then
|
|
219
|
+
# Check staged files for secrets
|
|
220
|
+
STAGED_CONTENT=$(git diff --cached 2>/dev/null || true)
|
|
221
|
+
if [[ -n "$STAGED_CONTENT" ]] && check_secrets "$STAGED_CONTENT"; then
|
|
222
|
+
{
|
|
223
|
+
echo "HOOK_BLOCKED: Hardcoded secret detected in staged changes"
|
|
224
|
+
echo " Use 'git commit --no-verify' to bypass if this is a false positive"
|
|
225
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
226
|
+
exit 2
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Check for sensitive files in commit
|
|
230
|
+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null || true)
|
|
231
|
+
if [[ -n "$STAGED_FILES" ]] && check_sensitive_files "$STAGED_FILES"; then
|
|
232
|
+
{
|
|
233
|
+
echo "HOOK_BLOCKED: Sensitive file in commit (${STAGED_FILES})"
|
|
234
|
+
echo " Files like .env, *.pem, *.key should not be committed"
|
|
235
|
+
echo " Use 'git commit --no-verify' to bypass if this is intentional"
|
|
236
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
237
|
+
exit 2
|
|
238
|
+
fi
|
|
239
|
+
fi
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# === QUALITY GUARDS (Phase 2) ===
|
|
244
|
+
|
|
245
|
+
# --- No-Changes Guard (AC-7) ---
|
|
246
|
+
# Block commits when there are no staged or unstaged changes (prevents empty commits)
|
|
247
|
+
# Skips for --amend since amending doesn't require new changes
|
|
248
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
249
|
+
if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
|
|
250
|
+
# Extract target directory from cd command if present (for worktree commits)
|
|
251
|
+
# Handles: "cd /path && git commit" or "cd /path; git commit"
|
|
252
|
+
TARGET_DIR=""
|
|
253
|
+
if echo "$TOOL_INPUT" | grep -qE '^cd [^;&|]+'; then
|
|
254
|
+
TARGET_DIR=$(echo "$TOOL_INPUT" | grep -oE '^cd [^;&|]+' | head -1 | sed 's/^cd //' | tr -d ' ')
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
# Check for changes in the target directory (or current if no cd)
|
|
258
|
+
if [[ -n "$TARGET_DIR" && -d "$TARGET_DIR" ]]; then
|
|
259
|
+
CHANGES=$(cd "$TARGET_DIR" && git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
260
|
+
else
|
|
261
|
+
CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
if [[ "$CHANGES" -eq 0 ]]; then
|
|
265
|
+
echo "HOOK_BLOCKED: No changes to commit. Stage files with 'git add' first." | tee -a /tmp/claude-hook.log >&2
|
|
266
|
+
exit 2
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# --- Worktree Validation (AC-8) ---
|
|
272
|
+
# Warn (but don't block) when committing outside a feature worktree
|
|
273
|
+
# This catches accidental commits to main repo during feature work
|
|
274
|
+
QUALITY_LOG="/tmp/claude-quality.log"
|
|
275
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
276
|
+
CWD=$(pwd)
|
|
277
|
+
if ! echo "$CWD" | grep -qE 'worktrees/feature/'; then
|
|
278
|
+
echo "$(date +%H:%M:%S) WORKTREE_WARNING: Committing outside feature worktree ($CWD)" >> "$QUALITY_LOG"
|
|
279
|
+
# Warning only - does not block
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
# --- Commit Message Validation (AC-3) ---
|
|
284
|
+
# Enforce conventional commits format: type(scope): description
|
|
285
|
+
# Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf
|
|
286
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
287
|
+
# Extract message from -m flag
|
|
288
|
+
MSG=""
|
|
289
|
+
|
|
290
|
+
# Try heredoc format first: -m "$(cat <<'EOF' ... EOF)"
|
|
291
|
+
# This is the most common format in Claude Code git commits
|
|
292
|
+
if echo "$TOOL_INPUT" | grep -qE "<<.*EOF"; then
|
|
293
|
+
# Extract first line after heredoc marker
|
|
294
|
+
MSG=$(echo "$TOOL_INPUT" | sed -n '/<<.*EOF/,/EOF/p' | sed '1d;$d' | head -1 | sed 's/^[[:space:]]*//')
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# Try -m "message" format (double quotes)
|
|
298
|
+
if [[ -z "$MSG" ]] && echo "$TOOL_INPUT" | grep -qE '\-m\s+"'; then
|
|
299
|
+
MSG=$(echo "$TOOL_INPUT" | awk -F'"' '{print $2}')
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Try -m 'message' format (single quotes)
|
|
303
|
+
if [[ -z "$MSG" ]] && echo "$TOOL_INPUT" | grep -qE "\-m\s+'"; then
|
|
304
|
+
MSG=$(echo "$TOOL_INPUT" | awk -F"'" '{print $2}')
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# Validate if we found a message
|
|
308
|
+
if [[ -n "$MSG" ]]; then
|
|
309
|
+
# Conventional commits pattern: type(optional-scope): description
|
|
310
|
+
# Also accepts ! for breaking changes: feat!: or feat(scope)!:
|
|
311
|
+
PATTERN='^(feat|fix|docs|style|refactor|test|chore|ci|build|perf)(\([^)]+\))?(!)?\s*:'
|
|
312
|
+
if ! echo "$MSG" | grep -qE "$PATTERN"; then
|
|
313
|
+
{
|
|
314
|
+
echo "HOOK_BLOCKED: Commit must follow conventional commits format"
|
|
315
|
+
echo " Expected: type(scope): description"
|
|
316
|
+
# AC-1 & AC-2 (Issue #198): Detect merge commits and provide helpful suggestion
|
|
317
|
+
if [[ "$MSG" == Merge\ * ]]; then
|
|
318
|
+
echo ""
|
|
319
|
+
echo " 💡 For merge commits, use: chore: merge main into feature branch"
|
|
320
|
+
echo ""
|
|
321
|
+
fi
|
|
322
|
+
echo " Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf"
|
|
323
|
+
echo " Got: $MSG"
|
|
324
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
325
|
+
exit 2
|
|
326
|
+
fi
|
|
327
|
+
fi
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# === WORKTREE PATH ENFORCEMENT ===
|
|
331
|
+
# Enforces that file operations stay within the designated worktree
|
|
332
|
+
# Sources for worktree path (in priority order):
|
|
333
|
+
# 1. SEQUANT_WORKTREE env var - set by `sequant run` for isolated issue execution
|
|
334
|
+
# 2. Parallel marker file - for parallel agent execution
|
|
335
|
+
# This prevents agents from accidentally editing the main repo instead of the worktree
|
|
336
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
337
|
+
EXPECTED_WORKTREE=""
|
|
338
|
+
|
|
339
|
+
# Priority 1: Check SEQUANT_WORKTREE environment variable (set by sequant run)
|
|
340
|
+
if [[ -n "${SEQUANT_WORKTREE:-}" ]]; then
|
|
341
|
+
EXPECTED_WORKTREE="$SEQUANT_WORKTREE"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# Priority 2: Fall back to parallel marker file
|
|
345
|
+
if [[ -z "$EXPECTED_WORKTREE" ]]; then
|
|
346
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
347
|
+
if [[ -f "$marker" ]]; then
|
|
348
|
+
# Read expected worktree path from marker file (first line)
|
|
349
|
+
EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
|
|
350
|
+
break
|
|
351
|
+
fi
|
|
352
|
+
done
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
if [[ -n "$EXPECTED_WORKTREE" ]]; then
|
|
356
|
+
# AC-4 (Issue #31): Check worktree directory exists before path validation
|
|
357
|
+
# Prevents Write tool from creating non-existent worktree directories
|
|
358
|
+
if [[ ! -d "$EXPECTED_WORKTREE" ]]; then
|
|
359
|
+
echo "HOOK_BLOCKED: Worktree does not exist: $EXPECTED_WORKTREE" | tee -a /tmp/claude-hook.log >&2
|
|
360
|
+
exit 2
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
FILE_PATH=""
|
|
364
|
+
if command -v jq &>/dev/null; then
|
|
365
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
|
|
366
|
+
fi
|
|
367
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
368
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | grep -oE '"file_path"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
369
|
+
fi
|
|
370
|
+
|
|
371
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
372
|
+
# Resolve to absolute path for consistent comparison
|
|
373
|
+
REAL_FILE_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
374
|
+
REAL_WORKTREE=$(realpath "$EXPECTED_WORKTREE" 2>/dev/null || echo "$EXPECTED_WORKTREE")
|
|
375
|
+
|
|
376
|
+
# Check if file path is within the expected worktree
|
|
377
|
+
if [[ "$REAL_FILE_PATH" != "$REAL_WORKTREE"* ]]; then
|
|
378
|
+
echo "$(date +%H:%M:%S) WORKTREE_BLOCKED: Edit outside expected worktree" >> "$QUALITY_LOG"
|
|
379
|
+
echo " Expected: $EXPECTED_WORKTREE" >> "$QUALITY_LOG"
|
|
380
|
+
echo " Got: $FILE_PATH" >> "$QUALITY_LOG"
|
|
381
|
+
{
|
|
382
|
+
echo "HOOK_BLOCKED: File operation must be within worktree"
|
|
383
|
+
echo " Worktree: $EXPECTED_WORKTREE"
|
|
384
|
+
echo " File: $FILE_PATH"
|
|
385
|
+
if [[ -n "${SEQUANT_ISSUE:-}" ]]; then
|
|
386
|
+
echo " Issue: #$SEQUANT_ISSUE"
|
|
387
|
+
fi
|
|
388
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
389
|
+
exit 2
|
|
390
|
+
fi
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# === FILE LOCKING FOR PARALLEL AGENTS (AC-6) ===
|
|
396
|
+
# Prevents concurrent edits to the same file when parallel agents are running
|
|
397
|
+
# Uses lockf (macOS native) with a per-file lock in /tmp
|
|
398
|
+
# Disabled with CLAUDE_HOOKS_FILE_LOCKING=false
|
|
399
|
+
if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
400
|
+
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
401
|
+
FILE_PATH=""
|
|
402
|
+
if command -v jq &>/dev/null; then
|
|
403
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty' 2>/dev/null)
|
|
404
|
+
fi
|
|
405
|
+
if [[ -z "$FILE_PATH" ]]; then
|
|
406
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | grep -oE '"file_path"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
if [[ -n "$FILE_PATH" ]]; then
|
|
410
|
+
# Create a lock file based on file path hash (handles special chars)
|
|
411
|
+
LOCK_FILE="/tmp/claude-lock-$(echo "$FILE_PATH" | md5 -q 2>/dev/null || echo "$FILE_PATH" | md5sum | cut -d' ' -f1).lock"
|
|
412
|
+
|
|
413
|
+
# Try to acquire lock with 30 second timeout
|
|
414
|
+
# Use a subshell to hold the lock during the tool execution
|
|
415
|
+
if command -v lockf &>/dev/null; then
|
|
416
|
+
# macOS: use lockf
|
|
417
|
+
exec 200>"$LOCK_FILE"
|
|
418
|
+
if ! lockf -t 30 200 2>/dev/null; then
|
|
419
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a /tmp/claude-hook.log >&2
|
|
420
|
+
exit 2
|
|
421
|
+
fi
|
|
422
|
+
# Lock will be released when the file descriptor closes (process exits)
|
|
423
|
+
elif command -v flock &>/dev/null; then
|
|
424
|
+
# Linux: use flock
|
|
425
|
+
exec 200>"$LOCK_FILE"
|
|
426
|
+
if ! flock -w 30 200 2>/dev/null; then
|
|
427
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a /tmp/claude-hook.log >&2
|
|
428
|
+
exit 2
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
# If neither lockf nor flock available, proceed without locking
|
|
432
|
+
fi
|
|
433
|
+
fi
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
# === PRE-MERGE WORKTREE CLEANUP ===
|
|
437
|
+
# Auto-remove worktree before `gh pr merge` to prevent --delete-branch failure
|
|
438
|
+
# The worktree locks the branch, causing merge to partially fail
|
|
439
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'gh pr merge'; then
|
|
440
|
+
# Extract PR number from command
|
|
441
|
+
PR_NUM=$(echo "$TOOL_INPUT" | grep -oE 'gh pr merge [0-9]+' | grep -oE '[0-9]+')
|
|
442
|
+
|
|
443
|
+
if [[ -n "$PR_NUM" ]]; then
|
|
444
|
+
# Get the branch name for this PR
|
|
445
|
+
BRANCH_NAME=$(gh pr view "$PR_NUM" --json headRefName --jq '.headRefName' 2>/dev/null || true)
|
|
446
|
+
|
|
447
|
+
if [[ -n "$BRANCH_NAME" ]]; then
|
|
448
|
+
# Check if a worktree exists for this branch
|
|
449
|
+
# Note: worktree line is 2 lines before branch line in porcelain output
|
|
450
|
+
WORKTREE_PATH=$(git worktree list --porcelain 2>/dev/null | grep -B2 "branch refs/heads/$BRANCH_NAME" | grep "^worktree " | sed 's/^worktree //' || true)
|
|
451
|
+
|
|
452
|
+
if [[ -n "$WORKTREE_PATH" && -d "$WORKTREE_PATH" ]]; then
|
|
453
|
+
# Remove the worktree before merge proceeds
|
|
454
|
+
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || true
|
|
455
|
+
echo "PRE-MERGE: Removed worktree $WORKTREE_PATH for branch $BRANCH_NAME" >> /tmp/claude-hook.log
|
|
456
|
+
fi
|
|
457
|
+
fi
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
# === ALLOW EVERYTHING ELSE ===
|
|
462
|
+
# Slash commands need: git, npm, file edits, gh pr/issue, MCP tools
|
|
463
|
+
exit 0
|