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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +14 -2
  3. package/README.md +2 -0
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
  6. package/dist/marketplace/external_plugins/sequant/README.md +38 -0
  7. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
  9. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
  10. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
  14. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
  27. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
  28. package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
  29. package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
  31. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
  34. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
  35. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
  36. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
  37. package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
  38. package/dist/src/commands/run.d.ts +13 -274
  39. package/dist/src/commands/run.js +43 -1958
  40. package/dist/src/commands/sync.js +3 -0
  41. package/dist/src/commands/update.js +3 -0
  42. package/dist/src/lib/plugin-version-sync.d.ts +2 -1
  43. package/dist/src/lib/plugin-version-sync.js +28 -7
  44. package/dist/src/lib/solve-comment-parser.d.ts +26 -0
  45. package/dist/src/lib/solve-comment-parser.js +63 -7
  46. package/dist/src/lib/upstream/assessment.js +6 -3
  47. package/dist/src/lib/upstream/relevance.d.ts +5 -0
  48. package/dist/src/lib/upstream/relevance.js +24 -0
  49. package/dist/src/lib/upstream/report.js +18 -46
  50. package/dist/src/lib/upstream/types.d.ts +2 -0
  51. package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
  52. package/dist/src/lib/workflow/batch-executor.js +574 -0
  53. package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
  54. package/dist/src/lib/workflow/phase-executor.js +381 -0
  55. package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
  56. package/dist/src/lib/workflow/phase-mapper.js +147 -0
  57. package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
  58. package/dist/src/lib/workflow/pr-operations.js +326 -0
  59. package/dist/src/lib/workflow/pr-status.d.ts +49 -0
  60. package/dist/src/lib/workflow/pr-status.js +131 -0
  61. package/dist/src/lib/workflow/run-reflect.d.ts +32 -0
  62. package/dist/src/lib/workflow/run-reflect.js +191 -0
  63. package/dist/src/lib/workflow/run-summary.d.ts +36 -0
  64. package/dist/src/lib/workflow/run-summary.js +142 -0
  65. package/dist/src/lib/workflow/state-cleanup.d.ts +79 -0
  66. package/dist/src/lib/workflow/state-cleanup.js +250 -0
  67. package/dist/src/lib/workflow/state-rebuild.d.ts +38 -0
  68. package/dist/src/lib/workflow/state-rebuild.js +140 -0
  69. package/dist/src/lib/workflow/state-utils.d.ts +14 -162
  70. package/dist/src/lib/workflow/state-utils.js +10 -677
  71. package/dist/src/lib/workflow/worktree-discovery.d.ts +61 -0
  72. package/dist/src/lib/workflow/worktree-discovery.js +229 -0
  73. package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
  74. package/dist/src/lib/workflow/worktree-manager.js +918 -0
  75. package/package.json +4 -2
  76. package/templates/skills/exec/SKILL.md +2 -2
  77. package/templates/skills/fullsolve/SKILL.md +15 -5
  78. package/templates/skills/loop/SKILL.md +1 -1
  79. package/templates/skills/qa/SKILL.md +47 -7
  80. package/templates/skills/solve/SKILL.md +92 -6
  81. package/templates/skills/spec/SKILL.md +57 -4
  82. package/templates/skills/test/SKILL.md +10 -0
  83. 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