sequant 1.5.6 → 1.6.1
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 |
|
|
@@ -19,7 +19,7 @@ export async function doctorCommand() {
|
|
|
19
19
|
message: `Outdated: ${versionResult.currentVersion} → ${versionResult.latestVersion} available`,
|
|
20
20
|
});
|
|
21
21
|
// Show remediation steps
|
|
22
|
-
console.log(chalk.yellow(` ⚠️ ${getVersionWarning(versionResult.currentVersion, versionResult.latestVersion)}`));
|
|
22
|
+
console.log(chalk.yellow(` ⚠️ ${getVersionWarning(versionResult.currentVersion, versionResult.latestVersion, versionResult.isLocalInstall)}`));
|
|
23
23
|
console.log("");
|
|
24
24
|
}
|
|
25
25
|
else {
|
package/dist/src/commands/run.js
CHANGED
|
@@ -742,7 +742,7 @@ export async function runCommand(issues, options) {
|
|
|
742
742
|
try {
|
|
743
743
|
const versionResult = await checkVersionCached();
|
|
744
744
|
if (versionResult.isOutdated && versionResult.latestVersion) {
|
|
745
|
-
console.log(chalk.yellow(` ⚠️ ${getVersionWarning(versionResult.currentVersion, versionResult.latestVersion)}`));
|
|
745
|
+
console.log(chalk.yellow(` ⚠️ ${getVersionWarning(versionResult.currentVersion, versionResult.latestVersion, versionResult.isLocalInstall)}`));
|
|
746
746
|
console.log("");
|
|
747
747
|
}
|
|
748
748
|
}
|
|
@@ -12,6 +12,7 @@ export interface VersionCheckResult {
|
|
|
12
12
|
currentVersion: string;
|
|
13
13
|
latestVersion: string | null;
|
|
14
14
|
isOutdated: boolean;
|
|
15
|
+
isLocalInstall?: boolean;
|
|
15
16
|
error?: string;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
@@ -26,6 +27,17 @@ export declare function getCachePath(): string;
|
|
|
26
27
|
* Get the current version from package.json
|
|
27
28
|
*/
|
|
28
29
|
export declare function getCurrentVersion(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Check if running from a local node_modules install (vs npx cache)
|
|
32
|
+
*
|
|
33
|
+
* Local installs are in: <project>/node_modules/sequant/
|
|
34
|
+
* npx installs are in: ~/.npm/_npx/<hash>/node_modules/sequant/
|
|
35
|
+
*
|
|
36
|
+
* This matters because:
|
|
37
|
+
* - Local installs should be updated with: npm update sequant
|
|
38
|
+
* - npx installs should be updated with: npx sequant@latest
|
|
39
|
+
*/
|
|
40
|
+
export declare function isLocalNodeModulesInstall(): boolean;
|
|
29
41
|
/**
|
|
30
42
|
* Read the version cache
|
|
31
43
|
*/
|
|
@@ -53,8 +65,11 @@ export declare function compareVersions(a: string, b: string): number;
|
|
|
53
65
|
export declare function isOutdated(currentVersion: string, latestVersion: string): boolean;
|
|
54
66
|
/**
|
|
55
67
|
* Get the version warning message
|
|
68
|
+
*
|
|
69
|
+
* For local node_modules installs, recommends `npm update sequant`
|
|
70
|
+
* For npx usage, recommends `npx sequant@latest`
|
|
56
71
|
*/
|
|
57
|
-
export declare function getVersionWarning(currentVersion: string, latestVersion: string): string;
|
|
72
|
+
export declare function getVersionWarning(currentVersion: string, latestVersion: string, isLocal?: boolean): string;
|
|
58
73
|
/**
|
|
59
74
|
* Check version freshness (thorough - for doctor command)
|
|
60
75
|
* Always fetches from npm registry
|
|
@@ -54,6 +54,25 @@ export function getCurrentVersion() {
|
|
|
54
54
|
return "0.0.0";
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Check if running from a local node_modules install (vs npx cache)
|
|
59
|
+
*
|
|
60
|
+
* Local installs are in: <project>/node_modules/sequant/
|
|
61
|
+
* npx installs are in: ~/.npm/_npx/<hash>/node_modules/sequant/
|
|
62
|
+
*
|
|
63
|
+
* This matters because:
|
|
64
|
+
* - Local installs should be updated with: npm update sequant
|
|
65
|
+
* - npx installs should be updated with: npx sequant@latest
|
|
66
|
+
*/
|
|
67
|
+
export function isLocalNodeModulesInstall() {
|
|
68
|
+
// Check if our path contains node_modules/sequant but NOT in .npm/_npx
|
|
69
|
+
const normalizedPath = __dirname.replace(/\\/g, "/");
|
|
70
|
+
// Running from local node_modules (not npx cache)
|
|
71
|
+
const inNodeModules = normalizedPath.includes("/node_modules/sequant");
|
|
72
|
+
const inNpxCache = normalizedPath.includes("/.npm/_npx/") ||
|
|
73
|
+
normalizedPath.includes("\\.npm\\_npx\\");
|
|
74
|
+
return inNodeModules && !inNpxCache;
|
|
75
|
+
}
|
|
57
76
|
/**
|
|
58
77
|
* Read the version cache
|
|
59
78
|
*/
|
|
@@ -158,10 +177,19 @@ export function isOutdated(currentVersion, latestVersion) {
|
|
|
158
177
|
}
|
|
159
178
|
/**
|
|
160
179
|
* Get the version warning message
|
|
180
|
+
*
|
|
181
|
+
* For local node_modules installs, recommends `npm update sequant`
|
|
182
|
+
* For npx usage, recommends `npx sequant@latest`
|
|
161
183
|
*/
|
|
162
|
-
export function getVersionWarning(currentVersion, latestVersion) {
|
|
184
|
+
export function getVersionWarning(currentVersion, latestVersion, isLocal) {
|
|
185
|
+
const isLocalInstall = isLocal ?? isLocalNodeModulesInstall();
|
|
186
|
+
if (isLocalInstall) {
|
|
187
|
+
return `sequant ${latestVersion} is available (you have ${currentVersion})
|
|
188
|
+
Run: npm update sequant
|
|
189
|
+
Note: You have sequant as a local dependency. npx uses your node_modules version.`;
|
|
190
|
+
}
|
|
163
191
|
return `sequant ${latestVersion} is available (you have ${currentVersion})
|
|
164
|
-
Run: npx sequant@latest
|
|
192
|
+
Run: npx sequant@latest`;
|
|
165
193
|
}
|
|
166
194
|
/**
|
|
167
195
|
* Check version freshness (thorough - for doctor command)
|
|
@@ -169,12 +197,14 @@ export function getVersionWarning(currentVersion, latestVersion) {
|
|
|
169
197
|
*/
|
|
170
198
|
export async function checkVersionThorough() {
|
|
171
199
|
const currentVersion = getCurrentVersion();
|
|
200
|
+
const isLocal = isLocalNodeModulesInstall();
|
|
172
201
|
const latestVersion = await fetchLatestVersion();
|
|
173
202
|
if (!latestVersion) {
|
|
174
203
|
return {
|
|
175
204
|
currentVersion,
|
|
176
205
|
latestVersion: null,
|
|
177
206
|
isOutdated: false,
|
|
207
|
+
isLocalInstall: isLocal,
|
|
178
208
|
error: "Could not fetch latest version",
|
|
179
209
|
};
|
|
180
210
|
}
|
|
@@ -184,6 +214,7 @@ export async function checkVersionThorough() {
|
|
|
184
214
|
currentVersion,
|
|
185
215
|
latestVersion,
|
|
186
216
|
isOutdated: isOutdated(currentVersion, latestVersion),
|
|
217
|
+
isLocalInstall: isLocal,
|
|
187
218
|
};
|
|
188
219
|
}
|
|
189
220
|
/**
|
|
@@ -192,6 +223,7 @@ export async function checkVersionThorough() {
|
|
|
192
223
|
*/
|
|
193
224
|
export async function checkVersionCached() {
|
|
194
225
|
const currentVersion = getCurrentVersion();
|
|
226
|
+
const isLocal = isLocalNodeModulesInstall();
|
|
195
227
|
// Check cache first
|
|
196
228
|
const cache = readCache();
|
|
197
229
|
if (cache && isCacheFresh(cache)) {
|
|
@@ -199,6 +231,7 @@ export async function checkVersionCached() {
|
|
|
199
231
|
currentVersion,
|
|
200
232
|
latestVersion: cache.latestVersion,
|
|
201
233
|
isOutdated: isOutdated(currentVersion, cache.latestVersion),
|
|
234
|
+
isLocalInstall: isLocal,
|
|
202
235
|
};
|
|
203
236
|
}
|
|
204
237
|
// Fetch new version (with timeout)
|
|
@@ -210,12 +243,14 @@ export async function checkVersionCached() {
|
|
|
210
243
|
currentVersion,
|
|
211
244
|
latestVersion: cache.latestVersion,
|
|
212
245
|
isOutdated: isOutdated(currentVersion, cache.latestVersion),
|
|
246
|
+
isLocalInstall: isLocal,
|
|
213
247
|
};
|
|
214
248
|
}
|
|
215
249
|
return {
|
|
216
250
|
currentVersion,
|
|
217
251
|
latestVersion: null,
|
|
218
252
|
isOutdated: false,
|
|
253
|
+
isLocalInstall: isLocal,
|
|
219
254
|
};
|
|
220
255
|
}
|
|
221
256
|
// Update cache
|
|
@@ -224,5 +259,6 @@ export async function checkVersionCached() {
|
|
|
224
259
|
currentVersion,
|
|
225
260
|
latestVersion,
|
|
226
261
|
isOutdated: isOutdated(currentVersion, latestVersion),
|
|
262
|
+
isLocalInstall: isLocal,
|
|
227
263
|
};
|
|
228
264
|
}
|
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
|