sequant 1.20.2 → 2.0.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 +2 -4
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -9
- package/dist/bin/cli.js +25 -2
- package/dist/src/commands/doctor.js +42 -9
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +52 -0
- package/dist/src/commands/logs.d.ts +1 -0
- package/dist/src/commands/logs.js +18 -2
- package/dist/src/commands/run.d.ts +7 -0
- package/dist/src/commands/run.js +235 -68
- package/dist/src/commands/serve.d.ts +13 -0
- package/dist/src/commands/serve.js +131 -0
- package/dist/src/commands/stats.d.ts +1 -0
- package/dist/src/commands/stats.js +185 -26
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +99 -50
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +4 -1
- package/dist/src/lib/ac-parser.d.ts +2 -0
- package/dist/src/lib/ac-parser.js +12 -2
- package/dist/src/lib/assess-comment-parser.d.ts +137 -0
- package/dist/src/lib/assess-comment-parser.js +344 -0
- package/dist/src/lib/ci/config.d.ts +22 -0
- package/dist/src/lib/ci/config.js +134 -0
- package/dist/src/lib/ci/index.d.ts +12 -0
- package/dist/src/lib/ci/index.js +10 -0
- package/dist/src/lib/ci/inputs.d.ts +29 -0
- package/dist/src/lib/ci/inputs.js +103 -0
- package/dist/src/lib/ci/labels.d.ts +34 -0
- package/dist/src/lib/ci/labels.js +101 -0
- package/dist/src/lib/ci/outputs.d.ts +25 -0
- package/dist/src/lib/ci/outputs.js +84 -0
- package/dist/src/lib/ci/triggers.d.ts +9 -0
- package/dist/src/lib/ci/triggers.js +86 -0
- package/dist/src/lib/ci/types.d.ts +131 -0
- package/dist/src/lib/ci/types.js +47 -0
- package/dist/src/lib/mcp-config.d.ts +54 -0
- package/dist/src/lib/mcp-config.js +172 -0
- package/dist/src/lib/merge-check/index.js +6 -12
- package/dist/src/lib/merge-check/types.d.ts +20 -7
- package/dist/src/lib/merge-check/types.js +11 -0
- package/dist/src/lib/phase-signal.d.ts +3 -3
- package/dist/src/lib/phase-signal.js +5 -3
- package/dist/src/lib/settings.d.ts +52 -0
- package/dist/src/lib/settings.js +41 -0
- package/dist/src/lib/shutdown.d.ts +16 -5
- package/dist/src/lib/shutdown.js +32 -12
- package/dist/src/lib/solve-comment-parser.d.ts +9 -102
- package/dist/src/lib/solve-comment-parser.js +13 -248
- package/dist/src/lib/stacks.d.ts +8 -0
- package/dist/src/lib/stacks.js +34 -0
- package/dist/src/lib/system.js +3 -7
- package/dist/src/lib/test-tautology-detector.d.ts +10 -0
- package/dist/src/lib/test-tautology-detector.js +43 -4
- package/dist/src/lib/upstream/assessment.js +9 -59
- package/dist/src/lib/upstream/issues.js +12 -75
- package/dist/src/lib/version-check.d.ts +2 -2
- package/dist/src/lib/version-check.js +6 -3
- package/dist/src/lib/version.d.ts +4 -0
- package/dist/src/lib/version.js +25 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
- package/dist/src/lib/workflow/batch-executor.js +232 -55
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
- package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
- package/dist/src/lib/workflow/drivers/aider.js +160 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
- package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
- package/dist/src/lib/workflow/drivers/index.js +27 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
- package/dist/src/lib/workflow/error-classifier.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +6 -3
- package/dist/src/lib/workflow/log-writer.js +57 -27
- package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
- package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
- package/dist/src/lib/workflow/phase-detection.js +45 -29
- package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
- package/dist/src/lib/workflow/phase-executor.js +345 -220
- package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
- package/dist/src/lib/workflow/phase-mapper.js +7 -7
- package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
- package/dist/src/lib/workflow/platforms/github.js +466 -0
- package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
- package/dist/src/lib/workflow/platforms/index.js +25 -0
- package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
- package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
- package/dist/src/lib/workflow/pr-status.d.ts +2 -4
- package/dist/src/lib/workflow/pr-status.js +3 -16
- package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
- package/dist/src/lib/workflow/qa-cache.js +88 -0
- package/dist/src/lib/workflow/reconcile.d.ts +69 -0
- package/dist/src/lib/workflow/reconcile.js +290 -0
- package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
- package/dist/src/lib/workflow/ring-buffer.js +37 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
- package/dist/src/lib/workflow/run-log-schema.js +47 -12
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/state-cleanup.js +21 -0
- package/dist/src/lib/workflow/state-manager.d.ts +34 -3
- package/dist/src/lib/workflow/state-manager.js +278 -126
- package/dist/src/lib/workflow/state-schema.d.ts +34 -30
- package/dist/src/lib/workflow/state-schema.js +35 -25
- package/dist/src/lib/workflow/state-utils.d.ts +3 -1
- package/dist/src/lib/workflow/state-utils.js +1 -0
- package/dist/src/lib/workflow/types.d.ts +208 -6
- package/dist/src/lib/workflow/types.js +20 -1
- package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
- package/dist/src/lib/workflow/worktree-discovery.js +6 -14
- package/dist/src/lib/workflow/worktree-manager.js +33 -51
- package/dist/src/mcp/index.d.ts +4 -0
- package/dist/src/mcp/index.js +4 -0
- package/dist/src/mcp/resources.d.ts +7 -0
- package/dist/src/mcp/resources.js +111 -0
- package/dist/src/mcp/run-registry.d.ts +34 -0
- package/dist/src/mcp/run-registry.js +42 -0
- package/dist/src/mcp/server.d.ts +12 -0
- package/dist/src/mcp/server.js +50 -0
- package/dist/src/mcp/tools/logs.d.ts +7 -0
- package/dist/src/mcp/tools/logs.js +149 -0
- package/dist/src/mcp/tools/run.d.ts +121 -0
- package/dist/src/mcp/tools/run.js +591 -0
- package/dist/src/mcp/tools/status.d.ts +7 -0
- package/dist/src/mcp/tools/status.js +127 -0
- package/package.json +10 -1
- package/templates/hooks/post-tool.sh +19 -8
- package/templates/hooks/pre-tool.sh +36 -49
- package/templates/mcp.json +6 -0
- package/templates/skills/assess/SKILL.md +354 -352
- package/templates/skills/exec/SKILL.md +64 -1
- package/templates/skills/fullsolve/SKILL.md +35 -4
- package/templates/skills/qa/SKILL.md +486 -9
- package/templates/skills/qa/scripts/quality-checks.sh +1 -1
- package/templates/skills/setup/SKILL.md +386 -0
- package/templates/skills/solve/SKILL.md +38 -664
- package/templates/skills/spec/SKILL.md +90 -31
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant_status MCP tool
|
|
3
|
+
*
|
|
4
|
+
* Get current workflow state for an issue.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { StateManager } from "../../lib/workflow/state-manager.js";
|
|
8
|
+
import { isRunning } from "../run-registry.js";
|
|
9
|
+
import { reconcileState, getNextActionHint, } from "../../lib/workflow/reconcile.js";
|
|
10
|
+
import { isExpired } from "../../lib/workflow/state-schema.js";
|
|
11
|
+
import { getSettings } from "../../lib/settings.js";
|
|
12
|
+
export function registerStatusTool(server) {
|
|
13
|
+
server.registerTool("sequant_status", {
|
|
14
|
+
title: "Sequant Status",
|
|
15
|
+
description: "Get the current workflow state, phase progress, and QA verdict for a tracked issue. " +
|
|
16
|
+
"Reconciles with GitHub on every call for accurate status. " +
|
|
17
|
+
"Use this to check whether an issue needs work before calling sequant_run. " +
|
|
18
|
+
"Returns isRunning: true when a sequant_run is actively executing — " +
|
|
19
|
+
"poll every 5-10 seconds during active runs for phase-level progress updates.",
|
|
20
|
+
annotations: {
|
|
21
|
+
readOnlyHint: true,
|
|
22
|
+
idempotentHint: true,
|
|
23
|
+
},
|
|
24
|
+
inputSchema: {
|
|
25
|
+
issue: z.number().describe("GitHub issue number to check status for"),
|
|
26
|
+
},
|
|
27
|
+
}, async ({ issue }) => {
|
|
28
|
+
if (!issue || issue <= 0) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
error: "INVALID_INPUT",
|
|
35
|
+
message: "A valid issue number is required",
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const stateManager = new StateManager();
|
|
44
|
+
// Reconcile state with GitHub before reading
|
|
45
|
+
const reconcileResult = await reconcileState({ stateManager });
|
|
46
|
+
stateManager.clearCache();
|
|
47
|
+
const issueState = await stateManager.getIssueState(issue);
|
|
48
|
+
// Respect TTL: treat expired resolved issues as not tracked
|
|
49
|
+
if (issueState) {
|
|
50
|
+
const settings = await getSettings();
|
|
51
|
+
const ttlDays = settings.run.resolvedIssueTTL ?? 7;
|
|
52
|
+
if (isExpired(issueState, ttlDays)) {
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: JSON.stringify({
|
|
58
|
+
issue,
|
|
59
|
+
status: "not_tracked",
|
|
60
|
+
isRunning: isRunning(issue),
|
|
61
|
+
lastSynced: reconcileResult.lastSynced,
|
|
62
|
+
githubReachable: reconcileResult.githubReachable,
|
|
63
|
+
message: `Issue #${issue} was resolved and has expired (TTL: ${ttlDays} days)`,
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!issueState) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
issue,
|
|
77
|
+
status: "not_tracked",
|
|
78
|
+
isRunning: isRunning(issue),
|
|
79
|
+
lastSynced: reconcileResult.lastSynced,
|
|
80
|
+
githubReachable: reconcileResult.githubReachable,
|
|
81
|
+
message: `Issue #${issue} is not currently tracked in workflow state`,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify({
|
|
92
|
+
issue,
|
|
93
|
+
title: issueState.title,
|
|
94
|
+
status: issueState.status,
|
|
95
|
+
isRunning: isRunning(issue),
|
|
96
|
+
currentPhase: issueState.currentPhase,
|
|
97
|
+
phases: issueState.phases,
|
|
98
|
+
worktree: issueState.worktree,
|
|
99
|
+
pr: issueState.pr,
|
|
100
|
+
lastActivity: issueState.lastActivity,
|
|
101
|
+
nextAction: getNextActionHint(issueState),
|
|
102
|
+
lastSynced: reconcileResult.lastSynced,
|
|
103
|
+
githubReachable: reconcileResult.githubReachable,
|
|
104
|
+
warnings: reconcileResult.warnings
|
|
105
|
+
.filter((w) => w.issueNumber === issue)
|
|
106
|
+
.map((w) => w.description),
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: JSON.stringify({
|
|
118
|
+
error: "STATE_ERROR",
|
|
119
|
+
message: error instanceof Error ? error.message : String(error),
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Quantize your development workflow - Sequential AI phases with quality gates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,6 +56,14 @@
|
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=20.0.0"
|
|
58
58
|
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"@modelcontextprotocol/sdk": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
59
67
|
"dependencies": {
|
|
60
68
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
|
61
69
|
"@hono/node-server": "^1.19.9",
|
|
@@ -70,6 +78,7 @@
|
|
|
70
78
|
"inquirer": "^13.3.0",
|
|
71
79
|
"open": "^11.0.0",
|
|
72
80
|
"ora": "^9.3.0",
|
|
81
|
+
"p-limit": "^7.3.0",
|
|
73
82
|
"yaml": "^2.7.0",
|
|
74
83
|
"zod": "^4.3.5"
|
|
75
84
|
},
|
|
@@ -38,24 +38,35 @@ else
|
|
|
38
38
|
TOOL_OUTPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_response"\s*:\s*\{[^}]+\}' | head -1)
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
_TMPDIR="${TMPDIR:-/tmp}"
|
|
42
|
+
|
|
43
|
+
# Use CLAUDE_PLUGIN_DATA for persistent logs (survives plugin updates)
|
|
44
|
+
if [[ -n "${CLAUDE_PLUGIN_DATA}" ]]; then
|
|
45
|
+
_LOG_DIR="${CLAUDE_PLUGIN_DATA}/logs"
|
|
46
|
+
mkdir -p "$_LOG_DIR"
|
|
47
|
+
else
|
|
48
|
+
_LOG_DIR="${_TMPDIR}"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
TIMING_LOG="${_LOG_DIR}/claude-timing.log"
|
|
52
|
+
QUALITY_LOG="${_LOG_DIR}/claude-quality.log"
|
|
53
|
+
TESTS_LOG="${_LOG_DIR}/claude-tests.log"
|
|
54
|
+
PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
|
|
45
55
|
|
|
46
56
|
# === AGENT ID DETECTION ===
|
|
47
57
|
# For parallel agents, detect group ID from marker files
|
|
48
|
-
# Format: /
|
|
58
|
+
# Format: ${_TMPDIR}/claude-parallel-<group-id>.marker
|
|
49
59
|
AGENT_ID=""
|
|
50
60
|
IS_PARALLEL_AGENT="false"
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
# Find marker files using find (works in both bash and zsh)
|
|
62
|
+
while IFS= read -r marker; do
|
|
63
|
+
if [[ -n "$marker" && -f "$marker" ]]; then
|
|
53
64
|
# Extract group ID from marker filename
|
|
54
65
|
AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
|
|
55
66
|
IS_PARALLEL_AGENT="true"
|
|
56
67
|
break
|
|
57
68
|
fi
|
|
58
|
-
done
|
|
69
|
+
done < <(find "${_TMPDIR}" -maxdepth 1 -name "claude-parallel-*.marker" 2>/dev/null)
|
|
59
70
|
|
|
60
71
|
# === TIMING END ===
|
|
61
72
|
# Include agent ID in log format if available (AC-4)
|
|
@@ -33,20 +33,32 @@ else
|
|
|
33
33
|
fi
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
_TMPDIR="${TMPDIR:-/tmp}"
|
|
37
|
+
|
|
38
|
+
# Use CLAUDE_PLUGIN_DATA for persistent logs (survives plugin updates)
|
|
39
|
+
if [[ -n "${CLAUDE_PLUGIN_DATA}" ]]; then
|
|
40
|
+
_LOG_DIR="${CLAUDE_PLUGIN_DATA}/logs"
|
|
41
|
+
mkdir -p "$_LOG_DIR"
|
|
42
|
+
else
|
|
43
|
+
_LOG_DIR="${_TMPDIR}"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
TIMING_LOG="${_LOG_DIR}/claude-timing.log"
|
|
47
|
+
HOOK_LOG="${_LOG_DIR}/claude-hook.log"
|
|
48
|
+
PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
|
|
38
49
|
|
|
39
50
|
# === AGENT ID DETECTION ===
|
|
40
51
|
# For parallel agents, detect group ID from marker files
|
|
41
|
-
# Format: /
|
|
52
|
+
# Format: ${_TMPDIR}/claude-parallel-<group-id>.marker
|
|
42
53
|
AGENT_ID=""
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
# Find marker files using find (works in both bash and zsh)
|
|
55
|
+
while IFS= read -r marker; do
|
|
56
|
+
if [[ -n "$marker" && -f "$marker" ]]; then
|
|
45
57
|
# Extract group ID from marker filename
|
|
46
58
|
AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.marker//')
|
|
47
59
|
break
|
|
48
60
|
fi
|
|
49
|
-
done
|
|
61
|
+
done < <(find "${_TMPDIR}" -maxdepth 1 -name "claude-parallel-*.marker" 2>/dev/null)
|
|
50
62
|
|
|
51
63
|
# === TIMING START ===
|
|
52
64
|
# Include agent ID in log format if available (AC-4)
|
|
@@ -75,38 +87,38 @@ if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
|
75
87
|
if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) '; then
|
|
76
88
|
# Pattern requires command to START with file reader (not match in quoted strings)
|
|
77
89
|
if echo "$TOOL_INPUT" | grep -qE '^(cat|less|head|tail|more) .*\.(env|pem|key)'; then
|
|
78
|
-
echo "HOOK_BLOCKED: Reading secret file" | tee -a
|
|
90
|
+
echo "HOOK_BLOCKED: Reading secret file" | tee -a "$HOOK_LOG" >&2
|
|
79
91
|
exit 2
|
|
80
92
|
fi
|
|
81
93
|
|
|
82
94
|
if echo "$TOOL_INPUT" | grep -qE '^(cat|less) .*~/\.(ssh|aws|gnupg|config/gh)'; then
|
|
83
|
-
echo "HOOK_BLOCKED: Reading credential directory" | tee -a
|
|
95
|
+
echo "HOOK_BLOCKED: Reading credential directory" | tee -a "$HOOK_LOG" >&2
|
|
84
96
|
exit 2
|
|
85
97
|
fi
|
|
86
98
|
fi
|
|
87
99
|
|
|
88
100
|
# Bare environment dump
|
|
89
101
|
if echo "$TOOL_INPUT" | grep -qE '^(env|printenv|export)$'; then
|
|
90
|
-
echo "HOOK_BLOCKED: Environment dump" | tee -a
|
|
102
|
+
echo "HOOK_BLOCKED: Environment dump" | tee -a "$HOOK_LOG" >&2
|
|
91
103
|
exit 2
|
|
92
104
|
fi
|
|
93
105
|
|
|
94
106
|
# Destructive system commands
|
|
95
107
|
if echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
|
|
96
|
-
echo "HOOK_BLOCKED: Destructive system command" | tee -a
|
|
108
|
+
echo "HOOK_BLOCKED: Destructive system command" | tee -a "$HOOK_LOG" >&2
|
|
97
109
|
exit 2
|
|
98
110
|
fi
|
|
99
111
|
|
|
100
112
|
# Deployment (should never happen in issue automation)
|
|
101
113
|
if echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
|
|
102
|
-
echo "HOOK_BLOCKED: Deployment command" | tee -a
|
|
114
|
+
echo "HOOK_BLOCKED: Deployment command" | tee -a "$HOOK_LOG" >&2
|
|
103
115
|
exit 2
|
|
104
116
|
fi
|
|
105
117
|
|
|
106
118
|
# Force push
|
|
107
119
|
# Pattern requires -f to be a standalone flag (not part of branch name like -fix)
|
|
108
120
|
if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
|
|
109
|
-
echo "HOOK_BLOCKED: Force push" | tee -a
|
|
121
|
+
echo "HOOK_BLOCKED: Force push" | tee -a "$HOOK_LOG" >&2
|
|
110
122
|
exit 2
|
|
111
123
|
fi
|
|
112
124
|
|
|
@@ -149,14 +161,14 @@ if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
|
|
|
149
161
|
echo " git stash # save changes"
|
|
150
162
|
echo " git merge --abort # cancel merge"
|
|
151
163
|
echo " Or run directly in terminal (outside Claude Code) to bypass"
|
|
152
|
-
} | tee -a
|
|
164
|
+
} | tee -a "$HOOK_LOG" >&2
|
|
153
165
|
exit 2
|
|
154
166
|
fi
|
|
155
167
|
fi
|
|
156
168
|
|
|
157
169
|
# CI/CD triggers (automation shouldn't trigger more automation)
|
|
158
170
|
if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
|
|
159
|
-
echo "HOOK_BLOCKED: Workflow trigger" | tee -a
|
|
171
|
+
echo "HOOK_BLOCKED: Workflow trigger" | tee -a "$HOOK_LOG" >&2
|
|
160
172
|
exit 2
|
|
161
173
|
fi
|
|
162
174
|
|
|
@@ -222,7 +234,7 @@ if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
|
|
|
222
234
|
{
|
|
223
235
|
echo "HOOK_BLOCKED: Hardcoded secret detected in staged changes"
|
|
224
236
|
echo " Use 'git commit --no-verify' to bypass if this is a false positive"
|
|
225
|
-
} | tee -a
|
|
237
|
+
} | tee -a "$HOOK_LOG" >&2
|
|
226
238
|
exit 2
|
|
227
239
|
fi
|
|
228
240
|
|
|
@@ -233,7 +245,7 @@ if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
|
|
|
233
245
|
echo "HOOK_BLOCKED: Sensitive file in commit (${STAGED_FILES})"
|
|
234
246
|
echo " Files like .env, *.pem, *.key should not be committed"
|
|
235
247
|
echo " Use 'git commit --no-verify' to bypass if this is intentional"
|
|
236
|
-
} | tee -a
|
|
248
|
+
} | tee -a "$HOOK_LOG" >&2
|
|
237
249
|
exit 2
|
|
238
250
|
fi
|
|
239
251
|
fi
|
|
@@ -262,7 +274,7 @@ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; t
|
|
|
262
274
|
fi
|
|
263
275
|
|
|
264
276
|
if [[ "$CHANGES" -eq 0 ]]; then
|
|
265
|
-
echo "HOOK_BLOCKED: No changes to commit. Stage files with 'git add' first." | tee -a
|
|
277
|
+
echo "HOOK_BLOCKED: No changes to commit. Stage files with 'git add' first." | tee -a "$HOOK_LOG" >&2
|
|
266
278
|
exit 2
|
|
267
279
|
fi
|
|
268
280
|
fi
|
|
@@ -271,7 +283,7 @@ fi
|
|
|
271
283
|
# --- Worktree Validation (AC-8) ---
|
|
272
284
|
# Warn (but don't block) when committing outside a feature worktree
|
|
273
285
|
# This catches accidental commits to main repo during feature work
|
|
274
|
-
QUALITY_LOG="/
|
|
286
|
+
QUALITY_LOG="${_LOG_DIR}/claude-quality.log"
|
|
275
287
|
if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
|
|
276
288
|
CWD=$(pwd)
|
|
277
289
|
if ! echo "$CWD" | grep -qE 'worktrees/feature/'; then
|
|
@@ -321,7 +333,7 @@ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; t
|
|
|
321
333
|
fi
|
|
322
334
|
echo " Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf"
|
|
323
335
|
echo " Got: $MSG"
|
|
324
|
-
} | tee -a
|
|
336
|
+
} | tee -a "$HOOK_LOG" >&2
|
|
325
337
|
exit 2
|
|
326
338
|
fi
|
|
327
339
|
fi
|
|
@@ -356,7 +368,7 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
|
356
368
|
# AC-4 (Issue #31): Check worktree directory exists before path validation
|
|
357
369
|
# Prevents Write tool from creating non-existent worktree directories
|
|
358
370
|
if [[ ! -d "$EXPECTED_WORKTREE" ]]; then
|
|
359
|
-
echo "HOOK_BLOCKED: Worktree does not exist: $EXPECTED_WORKTREE" | tee -a
|
|
371
|
+
echo "HOOK_BLOCKED: Worktree does not exist: $EXPECTED_WORKTREE" | tee -a "$HOOK_LOG" >&2
|
|
360
372
|
exit 2
|
|
361
373
|
fi
|
|
362
374
|
|
|
@@ -385,7 +397,7 @@ if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
|
|
|
385
397
|
if [[ -n "${SEQUANT_ISSUE:-}" ]]; then
|
|
386
398
|
echo " Issue: #$SEQUANT_ISSUE"
|
|
387
399
|
fi
|
|
388
|
-
} | tee -a
|
|
400
|
+
} | tee -a "$HOOK_LOG" >&2
|
|
389
401
|
exit 2
|
|
390
402
|
fi
|
|
391
403
|
fi
|
|
@@ -408,7 +420,7 @@ if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
|
408
420
|
|
|
409
421
|
if [[ -n "$FILE_PATH" ]]; then
|
|
410
422
|
# Create a lock file based on file path hash (handles special chars)
|
|
411
|
-
LOCK_FILE="/
|
|
423
|
+
LOCK_FILE="${_TMPDIR}/claude-lock-$(echo "$FILE_PATH" | md5 -q 2>/dev/null || echo "$FILE_PATH" | md5sum | cut -d' ' -f1).lock"
|
|
412
424
|
|
|
413
425
|
# Try to acquire lock with 30 second timeout
|
|
414
426
|
# Use a subshell to hold the lock during the tool execution
|
|
@@ -416,7 +428,7 @@ if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
|
416
428
|
# macOS: use lockf
|
|
417
429
|
exec 200>"$LOCK_FILE"
|
|
418
430
|
if ! lockf -t 30 200 2>/dev/null; then
|
|
419
|
-
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a
|
|
431
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a "$HOOK_LOG" >&2
|
|
420
432
|
exit 2
|
|
421
433
|
fi
|
|
422
434
|
# Lock will be released when the file descriptor closes (process exits)
|
|
@@ -424,7 +436,7 @@ if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
|
424
436
|
# Linux: use flock
|
|
425
437
|
exec 200>"$LOCK_FILE"
|
|
426
438
|
if ! flock -w 30 200 2>/dev/null; then
|
|
427
|
-
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a
|
|
439
|
+
echo "HOOK_BLOCKED: File locked by another agent: $FILE_PATH" | tee -a "$HOOK_LOG" >&2
|
|
428
440
|
exit 2
|
|
429
441
|
fi
|
|
430
442
|
fi
|
|
@@ -433,31 +445,6 @@ if [[ "${CLAUDE_HOOKS_FILE_LOCKING:-true}" == "true" ]]; then
|
|
|
433
445
|
fi
|
|
434
446
|
fi
|
|
435
447
|
|
|
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
448
|
# === ALLOW EVERYTHING ELSE ===
|
|
462
449
|
# Slash commands need: git, npm, file edits, gh pr/issue, MCP tools
|
|
463
450
|
exit 0
|