sequant 1.5.6 → 1.6.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/README.md +0 -1
- package/package.json +1 -1
- package/templates/hooks/pre-tool.sh +104 -13
package/README.md
CHANGED
|
@@ -121,7 +121,6 @@ npx sequant run 123 --quality-loop
|
|
|
121
121
|
|---------|---------|
|
|
122
122
|
| `/assess` | Issue triage and status assessment |
|
|
123
123
|
| `/docs` | Generate feature documentation |
|
|
124
|
-
| `/release` | Automated release workflow |
|
|
125
124
|
| `/clean` | Repository cleanup |
|
|
126
125
|
| `/security-review` | Deep security analysis |
|
|
127
126
|
| `/reflect` | Workflow improvement analysis |
|
package/package.json
CHANGED
|
@@ -108,6 +108,50 @@ if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
|
|
|
108
108
|
exit 2
|
|
109
109
|
fi
|
|
110
110
|
|
|
111
|
+
# --- Hard Reset Protection (Issue #85, enhanced) ---
|
|
112
|
+
# Block git reset --hard when there is local work that would be lost:
|
|
113
|
+
# - Unpushed commits on main/master
|
|
114
|
+
# - Uncommitted changes (staged or unstaged)
|
|
115
|
+
# - Unfinished merge in progress
|
|
116
|
+
if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
|
|
117
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
118
|
+
BLOCK_REASONS=""
|
|
119
|
+
|
|
120
|
+
# Check 1: Unpushed commits (only on main/master)
|
|
121
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
122
|
+
UNPUSHED=$(git log origin/$CURRENT_BRANCH..HEAD --oneline 2>/dev/null | wc -l | tr -d ' ')
|
|
123
|
+
if [[ "$UNPUSHED" -gt 0 ]]; then
|
|
124
|
+
BLOCK_REASONS="${BLOCK_REASONS} - $UNPUSHED unpushed commit(s) on $CURRENT_BRANCH\n"
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Check 2: Uncommitted changes (staged or unstaged)
|
|
129
|
+
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
130
|
+
if [[ "$UNCOMMITTED" -gt 0 ]]; then
|
|
131
|
+
BLOCK_REASONS="${BLOCK_REASONS} - $UNCOMMITTED uncommitted file(s)\n"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Check 3: Unfinished merge
|
|
135
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo ".git")
|
|
136
|
+
if [[ -f "$GIT_DIR/MERGE_HEAD" ]]; then
|
|
137
|
+
BLOCK_REASONS="${BLOCK_REASONS} - Unfinished merge in progress\n"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Block if any reasons found
|
|
141
|
+
if [[ -n "$BLOCK_REASONS" ]]; then
|
|
142
|
+
{
|
|
143
|
+
echo "HOOK_BLOCKED: git reset --hard would lose local work:"
|
|
144
|
+
echo -e "$BLOCK_REASONS"
|
|
145
|
+
echo " Resolve with:"
|
|
146
|
+
echo " git push origin $CURRENT_BRANCH # push commits"
|
|
147
|
+
echo " git stash # save changes"
|
|
148
|
+
echo " git merge --abort # cancel merge"
|
|
149
|
+
echo " Or run directly in terminal (outside Claude Code) to bypass"
|
|
150
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
151
|
+
exit 2
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
|
|
111
155
|
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
112
156
|
if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
|
|
113
157
|
echo "HOOK_BLOCKED: Workflow trigger" | tee -a /tmp/claude-hook.log >&2
|
|
@@ -273,22 +317,33 @@ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; t
|
|
|
273
317
|
fi
|
|
274
318
|
fi
|
|
275
319
|
|
|
276
|
-
# === WORKTREE PATH ENFORCEMENT
|
|
277
|
-
#
|
|
320
|
+
# === WORKTREE PATH ENFORCEMENT ===
|
|
321
|
+
# Enforces that file operations stay within the designated worktree
|
|
322
|
+
# Sources for worktree path (in priority order):
|
|
323
|
+
# 1. SEQUANT_WORKTREE env var - set by `sequant run` for isolated issue execution
|
|
324
|
+
# 2. Parallel marker file - for parallel agent execution
|
|
278
325
|
# This prevents agents from accidentally editing the main repo instead of the worktree
|
|
279
|
-
# Marker file format: First line contains the expected worktree path
|
|
280
326
|
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
281
327
|
EXPECTED_WORKTREE=""
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
328
|
+
|
|
329
|
+
# Priority 1: Check SEQUANT_WORKTREE environment variable (set by sequant run)
|
|
330
|
+
if [[ -n "${SEQUANT_WORKTREE:-}" ]]; then
|
|
331
|
+
EXPECTED_WORKTREE="$SEQUANT_WORKTREE"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# Priority 2: Fall back to parallel marker file
|
|
335
|
+
if [[ -z "$EXPECTED_WORKTREE" ]]; then
|
|
336
|
+
for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
|
|
337
|
+
if [[ -f "$marker" ]]; then
|
|
338
|
+
# Read expected worktree path from marker file (first line)
|
|
339
|
+
EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
|
|
340
|
+
break
|
|
341
|
+
fi
|
|
342
|
+
done
|
|
343
|
+
fi
|
|
289
344
|
|
|
290
345
|
if [[ -n "$EXPECTED_WORKTREE" ]]; then
|
|
291
|
-
# AC-
|
|
346
|
+
# AC-4 (Issue #31): Check worktree directory exists before path validation
|
|
292
347
|
# Prevents Write tool from creating non-existent worktree directories
|
|
293
348
|
if [[ ! -d "$EXPECTED_WORKTREE" ]]; then
|
|
294
349
|
echo "HOOK_BLOCKED: Worktree does not exist: $EXPECTED_WORKTREE" | tee -a /tmp/claude-hook.log >&2
|
|
@@ -304,12 +359,23 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
|
304
359
|
fi
|
|
305
360
|
|
|
306
361
|
if [[ -n "$FILE_PATH" ]]; then
|
|
362
|
+
# Resolve to absolute path for consistent comparison
|
|
363
|
+
REAL_FILE_PATH=$(realpath "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
|
|
364
|
+
REAL_WORKTREE=$(realpath "$EXPECTED_WORKTREE" 2>/dev/null || echo "$EXPECTED_WORKTREE")
|
|
365
|
+
|
|
307
366
|
# Check if file path is within the expected worktree
|
|
308
|
-
if
|
|
367
|
+
if [[ "$REAL_FILE_PATH" != "$REAL_WORKTREE"* ]]; then
|
|
309
368
|
echo "$(date +%H:%M:%S) WORKTREE_BLOCKED: Edit outside expected worktree" >> "$QUALITY_LOG"
|
|
310
369
|
echo " Expected: $EXPECTED_WORKTREE" >> "$QUALITY_LOG"
|
|
311
370
|
echo " Got: $FILE_PATH" >> "$QUALITY_LOG"
|
|
312
|
-
|
|
371
|
+
{
|
|
372
|
+
echo "HOOK_BLOCKED: File operation must be within worktree"
|
|
373
|
+
echo " Worktree: $EXPECTED_WORKTREE"
|
|
374
|
+
echo " File: $FILE_PATH"
|
|
375
|
+
if [[ -n "${SEQUANT_ISSUE:-}" ]]; then
|
|
376
|
+
echo " Issue: #$SEQUANT_ISSUE"
|
|
377
|
+
fi
|
|
378
|
+
} | tee -a /tmp/claude-hook.log >&2
|
|
313
379
|
exit 2
|
|
314
380
|
fi
|
|
315
381
|
fi
|
|
@@ -357,6 +423,31 @@ if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
|
357
423
|
fi
|
|
358
424
|
fi
|
|
359
425
|
|
|
426
|
+
# === PRE-MERGE WORKTREE CLEANUP ===
|
|
427
|
+
# Auto-remove worktree before `gh pr merge` to prevent --delete-branch failure
|
|
428
|
+
# The worktree locks the branch, causing merge to partially fail
|
|
429
|
+
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'gh pr merge'; then
|
|
430
|
+
# Extract PR number from command
|
|
431
|
+
PR_NUM=$(echo "$TOOL_INPUT" | grep -oE 'gh pr merge [0-9]+' | grep -oE '[0-9]+')
|
|
432
|
+
|
|
433
|
+
if [[ -n "$PR_NUM" ]]; then
|
|
434
|
+
# Get the branch name for this PR
|
|
435
|
+
BRANCH_NAME=$(gh pr view "$PR_NUM" --json headRefName --jq '.headRefName' 2>/dev/null || true)
|
|
436
|
+
|
|
437
|
+
if [[ -n "$BRANCH_NAME" ]]; then
|
|
438
|
+
# Check if a worktree exists for this branch
|
|
439
|
+
# Note: worktree line is 2 lines before branch line in porcelain output
|
|
440
|
+
WORKTREE_PATH=$(git worktree list --porcelain 2>/dev/null | grep -B2 "branch refs/heads/$BRANCH_NAME" | grep "^worktree " | sed 's/^worktree //' || true)
|
|
441
|
+
|
|
442
|
+
if [[ -n "$WORKTREE_PATH" && -d "$WORKTREE_PATH" ]]; then
|
|
443
|
+
# Remove the worktree before merge proceeds
|
|
444
|
+
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || true
|
|
445
|
+
echo "PRE-MERGE: Removed worktree $WORKTREE_PATH for branch $BRANCH_NAME" >> /tmp/claude-hook.log
|
|
446
|
+
fi
|
|
447
|
+
fi
|
|
448
|
+
fi
|
|
449
|
+
fi
|
|
450
|
+
|
|
360
451
|
# === ALLOW EVERYTHING ELSE ===
|
|
361
452
|
# Slash commands need: git, npm, file edits, gh pr/issue, MCP tools
|
|
362
453
|
exit 0
|