specline 1.1.2 → 1.1.3
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/package.json +1 -1
- package/templates/.cursor/hooks/specline-agent-guard.sh +28 -20
- package/templates/.cursor/hooks/specline-phase-guard.sh +21 -22
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +38 -1
- package/templates/.cursor/hooks/specline-reminder.sh +27 -19
- package/templates/.cursor/hooks/specline-session-start.sh +262 -80
- package/templates/.cursor/skills/specline-explore/SKILL.md +17 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +61 -0
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ set -euo pipefail
|
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
subagent_type=$(echo "$input" | jq -r '.subagent_type // empty')
|
|
15
|
+
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
15
16
|
|
|
16
17
|
# 非 specline agent → 放行(不受 Specline 管控)
|
|
17
18
|
if ! echo "$subagent_type" | grep -qE "^specline-"; then
|
|
@@ -32,29 +33,36 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
32
33
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
33
34
|
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
34
35
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
# 通过 session_id 解析绑定的 pipeline
|
|
37
|
+
resolve_pipeline_for_session() {
|
|
38
|
+
local session_id="$1"
|
|
39
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
40
|
+
|
|
41
|
+
if [ ! -f "$bindings_file" ]; then
|
|
42
|
+
return 1
|
|
40
43
|
fi
|
|
41
|
-
for f in "$CHANGES_DIR"/*/.pipeline-state.json; do
|
|
42
|
-
[ -f "$f" ] || continue
|
|
43
|
-
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
44
|
-
local ph
|
|
45
|
-
ph=$(jq -r '.current_phase // ""' "$f" 2>/dev/null)
|
|
46
|
-
if [ "$ph" != "archive" ] && [ "$ph" != "" ]; then
|
|
47
|
-
echo "$f"
|
|
48
|
-
return
|
|
49
|
-
fi
|
|
50
|
-
done
|
|
51
|
-
echo ""
|
|
52
|
-
}
|
|
53
44
|
|
|
54
|
-
|
|
45
|
+
local change_name
|
|
46
|
+
change_name=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$bindings_file" 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
if [ -z "$change_name" ]; then
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
local state_file="$CHANGES_DIR/$change_name/.pipeline-state.json"
|
|
53
|
+
|
|
54
|
+
if [ ! -f "$state_file" ]; then
|
|
55
|
+
# 脏数据:pipeline 已归档或不存在,清理绑定
|
|
56
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
57
|
+
return 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
STATE_FILE="$state_file"
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
if
|
|
64
|
+
STATE_FILE=""
|
|
65
|
+
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
58
66
|
echo '{"permission": "allow"}'
|
|
59
67
|
exit 0
|
|
60
68
|
fi
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
13
|
input=$(cat)
|
|
14
|
+
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
14
15
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
15
16
|
tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
|
|
16
17
|
|
|
@@ -18,27 +19,27 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
18
19
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
19
20
|
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
BINDINGS_FILE="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
23
|
+
[ ! -f "$BINDINGS_FILE" ] && echo '{}' > "$BINDINGS_FILE"
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
resolve_pipeline_for_session() {
|
|
26
|
+
local session_id="$1"
|
|
27
|
+
local change_name
|
|
28
|
+
change_name=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$BINDINGS_FILE" 2>/dev/null)
|
|
29
|
+
if [ -z "$change_name" ]; then
|
|
30
|
+
return 1
|
|
28
31
|
fi
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
fi
|
|
38
|
-
done
|
|
39
|
-
echo ""
|
|
32
|
+
local sf="$PROJECT_ROOT/specline/changes/$change_name/.pipeline-state.json"
|
|
33
|
+
if [ ! -f "$sf" ]; then
|
|
34
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$BINDINGS_FILE" > "${BINDINGS_FILE}.tmp" && mv "${BINDINGS_FILE}.tmp" "$BINDINGS_FILE"
|
|
35
|
+
return 1
|
|
36
|
+
fi
|
|
37
|
+
STATE_FILE="$sf"
|
|
38
|
+
return 0
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
# ===== 辅助函数 =====
|
|
42
|
+
|
|
42
43
|
deny() {
|
|
43
44
|
local user_msg="$1"
|
|
44
45
|
local agent_msg="$2"
|
|
@@ -60,11 +61,9 @@ allow() {
|
|
|
60
61
|
|
|
61
62
|
# ===== 主逻辑 =====
|
|
62
63
|
|
|
63
|
-
STATE_FILE
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
if [ -z "$STATE_FILE" ]; then
|
|
67
|
-
allow
|
|
64
|
+
STATE_FILE=""
|
|
65
|
+
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
66
|
+
allow # 无绑定 → 透明放行
|
|
68
67
|
exit 0
|
|
69
68
|
fi
|
|
70
69
|
|
|
@@ -595,6 +595,15 @@ gate_archive() {
|
|
|
595
595
|
sed -i '' "s/\"current_phase\": \"[^\"]*\"/\"current_phase\": \"archived\"/g" "$dest/.pipeline-state.json" 2>/dev/null || true
|
|
596
596
|
fi
|
|
597
597
|
|
|
598
|
+
# 清理所有绑定到该 change 的 session
|
|
599
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
600
|
+
if [ -f "$bindings_file" ]; then
|
|
601
|
+
jq --arg change "$CHANGE" \
|
|
602
|
+
'with_entries(select(.value.change != $change))' \
|
|
603
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
604
|
+
echo "✅ 已清理 pipeline '$CHANGE' 的所有 session 绑定"
|
|
605
|
+
fi
|
|
606
|
+
|
|
598
607
|
exit 0
|
|
599
608
|
fi
|
|
600
609
|
|
|
@@ -639,6 +648,31 @@ gate_status() {
|
|
|
639
648
|
}' "$STATE_FILE"
|
|
640
649
|
}
|
|
641
650
|
|
|
651
|
+
gate_bind() {
|
|
652
|
+
local session_id="$1"
|
|
653
|
+
local target_change="$2"
|
|
654
|
+
|
|
655
|
+
if [ -z "$session_id" ] || [ -z "$target_change" ]; then
|
|
656
|
+
fail "需要 <session_id> <change_name>"
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
local state_file="$PROJECT_ROOT/specline/changes/$target_change/.pipeline-state.json"
|
|
660
|
+
if [ ! -f "$state_file" ]; then
|
|
661
|
+
fail "Change '$target_change' 不存在"
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
665
|
+
[ ! -f "$bindings_file" ] && echo '{}' > "$bindings_file"
|
|
666
|
+
|
|
667
|
+
local now
|
|
668
|
+
now=$(now_iso8601)
|
|
669
|
+
jq --arg sid "$session_id" --arg change "$target_change" --arg time "$now" \
|
|
670
|
+
'.[$sid] = {"change": $change, "bound_at": $time}' \
|
|
671
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
672
|
+
|
|
673
|
+
echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
|
|
674
|
+
}
|
|
675
|
+
|
|
642
676
|
# ===== 分派 =====
|
|
643
677
|
|
|
644
678
|
case "$PHASE" in
|
|
@@ -669,9 +703,12 @@ case "$PHASE" in
|
|
|
669
703
|
test-e2e)
|
|
670
704
|
gate_test_e2e
|
|
671
705
|
;;
|
|
706
|
+
bind)
|
|
707
|
+
gate_bind "$2" "$3"
|
|
708
|
+
;;
|
|
672
709
|
archive)
|
|
673
710
|
gate_archive "$@"
|
|
674
|
-
;;
|
|
711
|
+
;;
|
|
675
712
|
status)
|
|
676
713
|
gate_status
|
|
677
714
|
;;
|
|
@@ -13,6 +13,7 @@ set -euo pipefail
|
|
|
13
13
|
input=$(cat)
|
|
14
14
|
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
15
15
|
tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
|
|
16
|
+
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
16
17
|
|
|
17
18
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
19
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
@@ -20,28 +21,35 @@ CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
|
20
21
|
|
|
21
22
|
# ===== 辅助函数 =====
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
resolve_pipeline_for_session() {
|
|
25
|
+
local session_id="$1"
|
|
26
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
27
|
+
|
|
28
|
+
if [ ! -f "$bindings_file" ]; then
|
|
29
|
+
return 1
|
|
27
30
|
fi
|
|
28
|
-
for f in "$CHANGES_DIR"/*/.pipeline-state.json; do
|
|
29
|
-
[ -f "$f" ] || continue
|
|
30
|
-
if echo "$f" | grep -q "/archive/"; then continue; fi
|
|
31
|
-
local ph
|
|
32
|
-
ph=$(jq -r '.current_phase // ""' "$f" 2>/dev/null)
|
|
33
|
-
if [ "$ph" != "archive" ] && [ "$ph" != "" ]; then
|
|
34
|
-
echo "$f"
|
|
35
|
-
return
|
|
36
|
-
fi
|
|
37
|
-
done
|
|
38
|
-
echo ""
|
|
39
|
-
}
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
local change_name
|
|
33
|
+
change_name=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$bindings_file" 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
if [ -z "$change_name" ]; then
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
local state_file="$CHANGES_DIR/$change_name/.pipeline-state.json"
|
|
40
|
+
|
|
41
|
+
if [ ! -f "$state_file" ]; then
|
|
42
|
+
# 脏数据:绑定存在但 pipeline 已不存在,清理绑定
|
|
43
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$bindings_file" > "$bindings_file.tmp" && mv "$bindings_file.tmp" "$bindings_file"
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
STATE_FILE="$state_file"
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
if
|
|
51
|
+
STATE_FILE=""
|
|
52
|
+
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
45
53
|
echo '{}'
|
|
46
54
|
exit 0
|
|
47
55
|
fi
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# specline-session-start.sh — sessionStart Hook
|
|
3
|
-
#
|
|
2
|
+
# specline-session-start.sh — sessionStart Hook (Pipeline Session Binding)
|
|
3
|
+
#
|
|
4
|
+
# 新会话启动时:
|
|
5
|
+
# 1. 清理过期绑定(7 天未更新)
|
|
6
|
+
# 2. 检查当前 session 是否已有绑定
|
|
7
|
+
# 3. 有绑定且 pipeline 仍活跃 → 使用已有绑定,注入上下文
|
|
8
|
+
# 4. 有绑定但 pipeline 已失效 → 清理脏数据,重新扫描
|
|
9
|
+
# 5. 无绑定 → 扫描活跃 pipeline:
|
|
10
|
+
# - 0 个 → 透明放行(echo '{}')
|
|
11
|
+
# - 1 个 → 自动绑定并注入上下文
|
|
12
|
+
# - 2+ 个 → 注入"请选择"提示
|
|
4
13
|
#
|
|
5
14
|
# Input (stdin JSON):
|
|
6
|
-
# { "session_id": "...", "is_background_agent": bool,
|
|
15
|
+
# { "session_id": "...", "is_background_agent": bool, ... }
|
|
7
16
|
#
|
|
8
17
|
# Output (stdout JSON):
|
|
9
18
|
# { "additional_context": "<pipeline 上下文>" } 或 {}(无活跃 pipeline)
|
|
10
19
|
|
|
11
20
|
set -euo pipefail
|
|
12
21
|
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# Input
|
|
24
|
+
# ============================================================================
|
|
25
|
+
|
|
13
26
|
input=$(cat)
|
|
14
27
|
|
|
15
28
|
# 跳过 background agent(子 Agent 不需要 pipeline 上下文)
|
|
@@ -19,116 +32,285 @@ if [ "$is_bg" = "true" ]; then
|
|
|
19
32
|
exit 0
|
|
20
33
|
fi
|
|
21
34
|
|
|
35
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
36
|
+
if [ -z "$session_id" ]; then
|
|
37
|
+
echo '{}'
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ============================================================================
|
|
42
|
+
# Paths
|
|
43
|
+
# ============================================================================
|
|
44
|
+
|
|
22
45
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
46
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
47
|
+
BINDINGS_FILE="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
24
48
|
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
25
49
|
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
exit 0
|
|
30
|
-
fi
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# Helper Functions
|
|
52
|
+
# ============================================================================
|
|
31
53
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
# init_bindings_file — ensure bindings file exists with a valid JSON object
|
|
55
|
+
init_bindings_file() {
|
|
56
|
+
if [ ! -f "$BINDINGS_FILE" ]; then
|
|
57
|
+
mkdir -p "$(dirname "$BINDINGS_FILE")"
|
|
58
|
+
echo '{}' > "$BINDINGS_FILE"
|
|
59
|
+
fi
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
# Validate it's readable JSON; reset if corrupted
|
|
62
|
+
if ! jq empty "$BINDINGS_FILE" 2>/dev/null; then
|
|
63
|
+
echo '{}' > "$BINDINGS_FILE"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
# clean_expired_bindings — remove bindings where bound_at > 7 days ago
|
|
68
|
+
# Uses ISO-8601 lexical comparison: works on both macOS and Linux
|
|
69
|
+
clean_expired_bindings() {
|
|
70
|
+
[ -f "$BINDINGS_FILE" ] || return 0
|
|
71
|
+
|
|
72
|
+
local cutoff_date
|
|
73
|
+
# macOS date, fallback to Linux date
|
|
74
|
+
cutoff_date=$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
|
|
75
|
+
date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null)
|
|
76
|
+
|
|
77
|
+
if [ -z "$cutoff_date" ]; then
|
|
78
|
+
return 0 # Cannot determine cutoff, skip cleanup
|
|
45
79
|
fi
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
82
|
+
# Keep only entries whose bound_at is >= cutoff (i.e. within the last 7 days)
|
|
83
|
+
jq --arg cutoff "$cutoff_date" \
|
|
84
|
+
'with_entries(select(.value.bound_at >= $cutoff))' \
|
|
85
|
+
"$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
86
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# is_pipeline_active — check if a pipeline's state file exists and is not archived
|
|
90
|
+
# Returns 0 (active) or 1 (inactive)
|
|
91
|
+
is_pipeline_active() {
|
|
92
|
+
local change_name="$1"
|
|
93
|
+
local state_file="$CHANGES_DIR/$change_name/.pipeline-state.json"
|
|
94
|
+
|
|
95
|
+
[ -f "$state_file" ] || return 1
|
|
96
|
+
|
|
97
|
+
local phase
|
|
48
98
|
phase=$(jq -r '.current_phase // "unknown"' "$state_file" 2>/dev/null)
|
|
49
|
-
|
|
99
|
+
[ "$phase" != "archive" ] || return 1
|
|
50
100
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
continue
|
|
54
|
-
fi
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
55
103
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
# scan_active_pipelines — scan changes/ (excluding archive/) for active pipelines
|
|
105
|
+
# Output: one line per pipeline: change_name|phase|state_file_path
|
|
106
|
+
scan_active_pipelines() {
|
|
107
|
+
if [ ! -d "$CHANGES_DIR" ]; then
|
|
108
|
+
return 0
|
|
60
109
|
fi
|
|
61
|
-
pipeline_count=$((pipeline_count + 1))
|
|
62
110
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
total=$(jq -r '[.phases.coding.tasks[]?] | length' "$state_file" 2>/dev/null || echo "0")
|
|
111
|
+
for state_file in "$CHANGES_DIR"/*/.pipeline-state.json; do
|
|
112
|
+
[ -f "$state_file" ] || continue
|
|
66
113
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
active_pipelines="${active_pipelines}\n"
|
|
72
|
-
done
|
|
114
|
+
# Exclude archive/
|
|
115
|
+
case "$state_file" in
|
|
116
|
+
*/archive/*) continue ;;
|
|
117
|
+
esac
|
|
73
118
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
local change_name phase
|
|
120
|
+
change_name=$(jq -r '.change_name // ""' "$state_file" 2>/dev/null)
|
|
121
|
+
phase=$(jq -r '.current_phase // ""' "$state_file" 2>/dev/null)
|
|
122
|
+
|
|
123
|
+
[ -n "$change_name" ] || continue
|
|
124
|
+
[ "$phase" != "archive" ] || continue
|
|
79
125
|
|
|
80
|
-
|
|
126
|
+
printf '%s|%s|%s\n' "$change_name" "$phase" "$state_file"
|
|
127
|
+
done
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# phase_constraints — output constraint text for a given phase (to stdout)
|
|
81
131
|
phase_constraints() {
|
|
82
|
-
|
|
132
|
+
local phase="$1"
|
|
133
|
+
case "$phase" in
|
|
83
134
|
spec)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
printf '%s\n' "- 只能通过 specline-spec-creator / specline-spec-reviewer 子 Agent 工作"
|
|
136
|
+
printf '%s\n' "- 禁止编辑任何应用代码文件(.ts/.tsx/.py/.go 等)"
|
|
137
|
+
printf '%s\n' "- 规划文件生成后需运行 Spec Gate"
|
|
87
138
|
;;
|
|
88
139
|
coding)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
140
|
+
printf '%s\n' "- 编码必须通过子 Agent:specline-frontend-dev / specline-backend-dev / specline-config-dev"
|
|
141
|
+
printf '%s\n' "- 禁止直接编辑应用代码文件"
|
|
142
|
+
printf '%s\n' "- 每批次任务完成后运行 Build Gate"
|
|
143
|
+
printf '%s\n' "- 每个 Task 完成后更新 tasks.md 的 checkbox"
|
|
93
144
|
;;
|
|
94
145
|
code_review)
|
|
95
|
-
|
|
96
|
-
|
|
146
|
+
printf '%s\n' "- 只能运行 specline-code-reviewer + Lint Gate"
|
|
147
|
+
printf '%s\n' "- 如需修复代码,通过子 Agent 完成"
|
|
97
148
|
;;
|
|
98
149
|
test)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
150
|
+
printf '%s\n' "- 运行测试 Gate 链:unit → integration → e2e"
|
|
151
|
+
printf '%s\n' "- 测试失败时通过 specline-test-runner 分析原因"
|
|
152
|
+
printf '%s\n' "- 代码修复通过子 Agent,测试修复通过 specline-test-writer"
|
|
102
153
|
;;
|
|
103
154
|
*)
|
|
104
|
-
|
|
155
|
+
printf '%s\n' "- 遵循 specline-pipeline SKILL 的当前阶段约束"
|
|
105
156
|
;;
|
|
106
157
|
esac
|
|
107
158
|
}
|
|
108
159
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else
|
|
120
|
-
context_text+="**活跃变更** (${pipeline_count} 个):\n${active_pipelines}\n"
|
|
121
|
-
fi
|
|
122
|
-
context_text+="**阶段约束**:\n${constraints}\n\n"
|
|
123
|
-
context_text+="**重要**: 你是 Specline Pipeline 编排者。上述约束具有最高优先级,必须在每个操作前检查是否符合当前阶段要求。"
|
|
160
|
+
# build_context — generate a JSON-escaped additional_context string for a bound pipeline
|
|
161
|
+
build_context() {
|
|
162
|
+
local change_name="$1"
|
|
163
|
+
local phase="$2"
|
|
164
|
+
local state_file="$3"
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
printf '🚨 **Specline Pipeline 运行中**\n\n'
|
|
168
|
+
printf '**当前变更**: %s\n' "$change_name"
|
|
169
|
+
printf '**当前阶段**: %s\n' "$phase"
|
|
124
170
|
|
|
125
|
-
#
|
|
126
|
-
|
|
171
|
+
# Task progress for coding phase
|
|
172
|
+
if [ "$phase" = "coding" ] && [ -f "$state_file" ]; then
|
|
173
|
+
local completed total
|
|
174
|
+
completed=$(jq -r '[.phases.coding.tasks[]? | select(.status == "completed")] | length' "$state_file" 2>/dev/null || printf '0')
|
|
175
|
+
total=$(jq -r '[.phases.coding.tasks[]?] | length' "$state_file" 2>/dev/null || printf '0')
|
|
176
|
+
if [ "$total" != "0" ]; then
|
|
177
|
+
printf '**任务进度**: %s/%s 完成\n' "$completed" "$total"
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
127
180
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
printf '\n**阶段约束**:\n'
|
|
182
|
+
phase_constraints "$phase"
|
|
183
|
+
printf '\n'
|
|
184
|
+
printf '**重要**: 你是 Specline Pipeline 编排者。上述约束具有最高优先级,必须在每个操作前检查是否符合当前阶段要求。'
|
|
185
|
+
} | jq -Rs '.'
|
|
131
186
|
}
|
|
132
|
-
EOF
|
|
133
187
|
|
|
188
|
+
# phase_constraint_table — static phase constraint reference table (markdown)
|
|
189
|
+
phase_constraint_table() {
|
|
190
|
+
cat << 'TABLEEOF'
|
|
191
|
+
| 阶段 | 约束 |
|
|
192
|
+
|------|------|
|
|
193
|
+
| spec | 只能通过 specline-spec-creator/specline-spec-reviewer 子 Agent 工作,禁止编辑代码 |
|
|
194
|
+
| coding | 必须通过子 Agent 编码,批次完成后运行 Build Gate,更新 tasks.md |
|
|
195
|
+
| code_review | 只能运行 specline-code-reviewer + Lint Gate |
|
|
196
|
+
| test | 运行测试 Gate 链:unit → integration → e2e |
|
|
197
|
+
TABLEEOF
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# write_binding — write a session_id → change_name binding to .pipeline-sessions.json
|
|
201
|
+
write_binding() {
|
|
202
|
+
local sid="$1"
|
|
203
|
+
local change="$2"
|
|
204
|
+
|
|
205
|
+
local now_iso
|
|
206
|
+
now_iso=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
207
|
+
|
|
208
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
209
|
+
jq --arg sid "$sid" --arg change "$change" --arg now "$now_iso" \
|
|
210
|
+
'.[$sid] = {"change": $change, "bound_at": $now}' \
|
|
211
|
+
"$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
212
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# delete_binding — remove a session_id entry from .pipeline-sessions.json
|
|
216
|
+
delete_binding() {
|
|
217
|
+
local session_id="$1"
|
|
218
|
+
|
|
219
|
+
[ -f "$BINDINGS_FILE" ] || return 0
|
|
220
|
+
|
|
221
|
+
local tmp_file="${BINDINGS_FILE}.tmp"
|
|
222
|
+
jq --arg sid "$session_id" 'del(.[$sid])' "$BINDINGS_FILE" > "$tmp_file" 2>/dev/null && \
|
|
223
|
+
mv "$tmp_file" "$BINDINGS_FILE" 2>/dev/null || true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# ============================================================================
|
|
227
|
+
# Main Logic
|
|
228
|
+
# ============================================================================
|
|
229
|
+
|
|
230
|
+
init_bindings_file
|
|
231
|
+
|
|
232
|
+
# 1. Clean expired bindings (bound_at > 7 days ago)
|
|
233
|
+
clean_expired_bindings
|
|
234
|
+
|
|
235
|
+
# 2. Check existing binding for this session
|
|
236
|
+
existing_change=""
|
|
237
|
+
if [ -f "$BINDINGS_FILE" ]; then
|
|
238
|
+
existing_change=$(jq -r --arg sid "$session_id" '.[$sid].change // empty' "$BINDINGS_FILE" 2>/dev/null)
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
if [ -n "$existing_change" ]; then
|
|
242
|
+
if is_pipeline_active "$existing_change"; then
|
|
243
|
+
# Binding is still valid — use it
|
|
244
|
+
state_file="$CHANGES_DIR/$existing_change/.pipeline-state.json"
|
|
245
|
+
phase=$(jq -r '.current_phase // "unknown"' "$state_file" 2>/dev/null)
|
|
246
|
+
|
|
247
|
+
ctx_json=$(build_context "$existing_change" "$phase" "$state_file")
|
|
248
|
+
|
|
249
|
+
printf '{\n "additional_context": %s\n}\n' "$ctx_json"
|
|
250
|
+
exit 0
|
|
251
|
+
else
|
|
252
|
+
# Dirty data: pipeline archived or deleted — clean up and rescan
|
|
253
|
+
delete_binding "$session_id"
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
# 3. No (valid) binding — scan active pipelines
|
|
258
|
+
scan_result=$(scan_active_pipelines)
|
|
259
|
+
|
|
260
|
+
# Count active pipelines
|
|
261
|
+
pipeline_count=0
|
|
262
|
+
if [ -n "$scan_result" ]; then
|
|
263
|
+
pipeline_count=$(printf '%s\n' "$scan_result" | wc -l | tr -d ' ')
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# 0 active pipelines → transparent pass-through
|
|
267
|
+
if [ "$pipeline_count" -eq 0 ]; then
|
|
268
|
+
echo '{}'
|
|
269
|
+
exit 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# 1 active pipeline → auto-bind and inject context
|
|
273
|
+
if [ "$pipeline_count" -eq 1 ]; then
|
|
274
|
+
change_name=$(printf '%s' "$scan_result" | cut -d'|' -f1)
|
|
275
|
+
phase=$(printf '%s' "$scan_result" | cut -d'|' -f2)
|
|
276
|
+
state_file=$(printf '%s' "$scan_result" | cut -d'|' -f3)
|
|
277
|
+
|
|
278
|
+
# Auto-bind
|
|
279
|
+
write_binding "$session_id" "$change_name"
|
|
280
|
+
|
|
281
|
+
# Inject context
|
|
282
|
+
ctx_json=$(build_context "$change_name" "$phase" "$state_file")
|
|
283
|
+
|
|
284
|
+
printf '{\n "additional_context": %s\n}\n' "$ctx_json"
|
|
285
|
+
exit 0
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# 4. 2+ active pipelines → inject selection prompt (do NOT auto-bind)
|
|
289
|
+
ctx_json=$({
|
|
290
|
+
printf '🚨 **Specline Pipeline 运行中 — 请选择 Pipeline**\n\n'
|
|
291
|
+
printf '**活跃 Pipeline** (%s 个):\n' "$pipeline_count"
|
|
292
|
+
|
|
293
|
+
while IFS='|' read -r change_name phase state_file; do
|
|
294
|
+
[ -n "$change_name" ] || continue
|
|
295
|
+
printf -- '- **%s**: %s 阶段' "$change_name" "$phase"
|
|
296
|
+
|
|
297
|
+
# Add task progress for coding phase
|
|
298
|
+
if [ "$phase" = "coding" ] && [ -f "$state_file" ]; then
|
|
299
|
+
completed=$(jq -r '[.phases.coding.tasks[]? | select(.status == "completed")] | length' "$state_file" 2>/dev/null || printf '0')
|
|
300
|
+
total=$(jq -r '[.phases.coding.tasks[]?] | length' "$state_file" 2>/dev/null || printf '0')
|
|
301
|
+
if [ "$total" != "0" ]; then
|
|
302
|
+
printf ' (%s/%s 任务完成)' "$completed" "$total"
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
printf '\n'
|
|
306
|
+
done <<< "$scan_result"
|
|
307
|
+
|
|
308
|
+
printf '\n**说明**: 当前发现了 %s 个活跃 Pipeline,但尚未绑定到本会话。\n' "$pipeline_count"
|
|
309
|
+
printf '**操作**: 请使用 `/specline-pipeline --change <名称>` 选择一个 Pipeline 以继续工作。\n\n'
|
|
310
|
+
printf '**阶段约束参考**:\n'
|
|
311
|
+
phase_constraint_table
|
|
312
|
+
printf '\n**提示**: 绑定后系统会自动注入正确的阶段上下文和约束。\n'
|
|
313
|
+
} | jq -Rs '.')
|
|
314
|
+
|
|
315
|
+
printf '{\n "additional_context": %s\n}\n' "$ctx_json"
|
|
134
316
|
exit 0
|
|
@@ -9,6 +9,23 @@ metadata:
|
|
|
9
9
|
generatedBy: "1.3.1"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## ⚠️ Mode Check — Read Before Anything Else
|
|
13
|
+
|
|
14
|
+
探索模式 MUST run in **Ask Mode** for hard read-only enforcement by Cursor.
|
|
15
|
+
|
|
16
|
+
### Your very first action
|
|
17
|
+
|
|
18
|
+
1. Check which Cursor mode you are currently in (agent / ask / plan).
|
|
19
|
+
2. **If you are NOT in Ask Mode** — Output ONLY the following message and STOP. Do not explore, do not read files, do not do anything else:
|
|
20
|
+
|
|
21
|
+
> 探索模式需要在只读(Ask)模式下运行。请点击输入框左侧的模式选择器,切换到 **Ask 模式**,然后重新输入 `/specline-explore`。
|
|
22
|
+
|
|
23
|
+
3. **If you are in Ask Mode** — You are in a safe, read-only environment. The system guarantees you cannot edit files, create files, rewrite files, delete files, or run commands. Proceed with exploration freely.
|
|
24
|
+
|
|
25
|
+
### Why this matters
|
|
26
|
+
|
|
27
|
+
Ask Mode is Cursor's only truly read-only mode. In Agent/Plan mode, only Prompt-level instructions prevent code changes — and those can be ignored. Ask Mode enforces read-only at the system level, making it impossible to accidentally modify files during exploration.
|
|
28
|
+
|
|
12
29
|
> **One-liner**: You're a thinking partner, not an implementer.
|
|
13
30
|
> **What you can do**: Read code, draw diagrams, compare options, ask questions, challenge assumptions
|
|
14
31
|
> **What you can't do**: Write implementation code
|
|
@@ -9,6 +9,67 @@ description: >-
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## Layer 0: Session 绑定与 Pipeline 切换
|
|
13
|
+
|
|
14
|
+
### 概述
|
|
15
|
+
|
|
16
|
+
每个 Cursor 会话通过 `session_id` 绑定到特定的 Pipeline。Hook 脚本通过查 `specline/.pipeline-sessions.json` 表获得确定性映射,替代原有的非确定性 `find_active_pipeline()` 扫描逻辑。
|
|
17
|
+
|
|
18
|
+
### 启动时自动绑定
|
|
19
|
+
|
|
20
|
+
`sessionStart` Hook (`specline-session-start.sh`) 自动处理:
|
|
21
|
+
|
|
22
|
+
1. **已绑定且有效** → 直接使用,注入正确的阶段约束
|
|
23
|
+
2. **无绑定 + 1 个活跃 Pipeline** → 自动绑定当前 session 到该 Pipeline
|
|
24
|
+
3. **无绑定 + 2+ 个活跃 Pipeline** → 注入提示,编排者必须用 AskUserQuestion 让用户选择
|
|
25
|
+
4. **过期绑定清理** → `bound_at` 超过 7 天的绑定自动删除
|
|
26
|
+
5. **脏数据清理** → 绑定指向已归档/不存在的 Pipeline 时自动删除
|
|
27
|
+
|
|
28
|
+
### 编排者收到「多 Pipeline 未绑定」提示时
|
|
29
|
+
|
|
30
|
+
当 sessionStart 注入上下文提示有多个 pipeline 未绑定时,编排者**必须**使用 AskUserQuestion 让用户选择:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
AskUserQuestion({
|
|
34
|
+
title: "选择 Pipeline",
|
|
35
|
+
questions: [{
|
|
36
|
+
id: "pipeline_select",
|
|
37
|
+
prompt: "当前有 " + count + " 个活跃 Pipeline,请选择要绑定到本会话的:",
|
|
38
|
+
options: [
|
|
39
|
+
{ id: "change-a", label: "change-a (SPEC 阶段)" },
|
|
40
|
+
{ id: "change-b", label: "change-b (CODING 阶段, 3/7 任务)" },
|
|
41
|
+
],
|
|
42
|
+
allow_multiple: false
|
|
43
|
+
}]
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
用户选择后,编排者执行绑定命令:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
.cursor/hooks/specline-pipeline-gate.sh bind <session_id> <selected_change>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 用户要求切换 Pipeline 时
|
|
54
|
+
|
|
55
|
+
当用户在对话中说「帮我处理 \<other-change\>」:
|
|
56
|
+
|
|
57
|
+
1. 检查当前 Pipeline 是否在安全切换点(Gate 之后、批次之间)
|
|
58
|
+
2. 如果当前在 CODING 批次中间 → 提示「请先完成当前批次」
|
|
59
|
+
3. 否则执行切换:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
.cursor/hooks/specline-pipeline-gate.sh bind <session_id> <other_change>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
绑定后,下一个 Hook 调用立即生效。
|
|
66
|
+
|
|
67
|
+
### Pipeline 归档时自动解绑
|
|
68
|
+
|
|
69
|
+
`gate archive --execute` 会自动清理所有绑定到该 Pipeline 的 session 记录,无需手工操作。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
12
73
|
## Layer 1: 速览与定位
|
|
13
74
|
|
|
14
75
|
你是**流水线编排者**,不是执行者。
|