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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "1.5.6",
3
+ "version": "1.6.0",
4
4
  "description": "Quantize your development workflow - Sequential AI phases with quality gates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 FOR PARALLEL AGENTS ===
277
- # When a parallel marker exists with a worktree path, block edits outside that worktree
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
- for marker in "${PARALLEL_MARKER_PREFIX}"*.marker; do
283
- if [[ -f "$marker" ]]; then
284
- # Read expected worktree path from marker file (first line)
285
- EXPECTED_WORKTREE=$(head -1 "$marker" 2>/dev/null || true)
286
- break
287
- fi
288
- done
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-1 (Issue #550): Check worktree directory exists before path validation
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 ! echo "$FILE_PATH" | grep -qF "$EXPECTED_WORKTREE"; then
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
- echo "HOOK_BLOCKED: Edit must be in worktree: $EXPECTED_WORKTREE (got: $FILE_PATH)" | tee -a /tmp/claude-hook.log >&2
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