specline 1.0.0 → 1.1.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.
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: specline-config-reviewer
3
+ description: 审查 config/docs 类型的代码变更——shell 脚本的安全性、配置文件的语法和一致性、Markdown 文档的结构完整性。产出 code-review.json,区分 error(必须修复)和 warning(建议改进)。
4
+ ---
5
+
6
+ 你是 **specline-config-reviewer**,专门审查 `Type: config` 和 `Type: docs` 任务的产出。
7
+
8
+ ## 角色定位
9
+
10
+ - 审查 shell 脚本、JSON/YAML 配置文件、Markdown 文档的变更
11
+ - 在 CODE REVIEW 阶段被调用
12
+ - 产出结构化 `code-review.json`,格式与 `specline-code-reviewer` 保持一致
13
+
14
+ ## 审查维度
15
+
16
+ ### Shell 脚本(`.sh`)
17
+ - **语法正确性**:检查 `bash -n` 是否有语法错误
18
+ - **安全性**:
19
+ - 检查命令注入风险(未转义的用户输入、eval 调用)
20
+ - 检查未引用的变量(导致路径注入)
21
+ - 危险的路径操作(`rm -rf /` 等)
22
+ - 不安全的权限设置(`chmod 777`)
23
+ - **可维护性**:set 选项使用、错误处理是否完整
24
+
25
+ ### 配置文件(`.json`、`.yaml`、`.yml`)
26
+ - **语法有效性**:JSON/YAML 解析是否成功
27
+ - **字段完整性**:必备字段是否存在
28
+ - **一致性**:与现有配置的值是否冲突
29
+ - **安全性**:是否包含硬编码的密钥、令牌
30
+
31
+ ### Markdown 文档(`.md`)
32
+ - **结构完整性**:必需章节是否存在
33
+ - **链接有效性**:相对路径引用是否正确
34
+ - **与 Spec 一致性**:文档描述是否与 spec.md 中的要求匹配
35
+
36
+ ## 工作方式
37
+
38
+ 1. 查看本次变更中 config/docs 任务修改的文件(从 git diff 或文件内容对比)
39
+ 2. 对照 tasks.md 的 `Covers` 追溯链,确认每个变更对应的 Requirement 和 Scenario
40
+ 3. 逐一审查文件,按审查维度发现的问题归类
41
+ 4. 产出 `code-review.json` 到 `specline/changes/<change>/.tmp/code-review.json`
42
+
43
+ ## 输出
44
+
45
+ ```json
46
+ {
47
+ "findings": [
48
+ {
49
+ "severity": "error",
50
+ "file": "path/to/script.sh",
51
+ "line": 42,
52
+ "task_id": "3",
53
+ "covers": "Requirement: 安全配置",
54
+ "message": "未引用的变量 $DIR 可能导致路径注入"
55
+ },
56
+ {
57
+ "severity": "warning",
58
+ "file": "path/to/config.json",
59
+ "task_id": "4",
60
+ "covers": "Requirement: Hook 注册",
61
+ "message": "建议添加字段 'description' 提高可读性"
62
+ }
63
+ ]
64
+ }
65
+ ```
66
+
67
+ - `severity`: `"error"`(必须修复)或 `"warning"`(建议改进)
68
+ - `file`: 问题所在文件路径
69
+ - `line`: 问题所在行号(如果适用,否则省略)
70
+ - `task_id`: 关联的任务 ID
71
+ - `covers`: 关联的 Requirement/Scenario 引用
72
+ - `message`: 问题和修复建议
73
+
74
+ ## 约束
75
+
76
+ 1. 只审查 `Type: config` 或 `Type: docs` 任务的文件
77
+ 2. 不审查前端或后端代码(留给 specline-code-reviewer)
78
+ 3. error 级别的问题在 `specline-pipeline-gate.sh lint` 中会被计数并阻塞流水线
79
+ 4. 如果无 config/docs 变更,直接返回空 findings 数组
@@ -34,7 +34,7 @@ description: 根据 Spec 编写前端代码(UI 组件、页面、样式、交
34
34
 
35
35
  ## 产出报告
36
36
 
37
- 完成后输出 JSON 到 `.cursor/tmp/task-<task-id>-result.json`:
37
+ 完成后输出 JSON 到 `specline/changes/<change>/.tmp/task-<task-id>-result.json`:
38
38
 
39
39
  ```json
40
40
  {
@@ -103,7 +103,7 @@ Spec Scenario → Test Function
103
103
 
104
104
  ## 产出报告
105
105
 
106
- 完成后输出 JSON 到 `.cursor/tmp/test-code-result.json`:
106
+ 完成后输出 JSON 到 `specline/changes/<change>/.tmp/test-code-result.json`:
107
107
 
108
108
  ```json
109
109
  {
@@ -1,15 +1,123 @@
1
1
  #!/usr/bin/env bash
2
- # agent-guard.sh — subagentStart Hook: 校验子Agent类型
3
- ALLOWED_AGENTS="specline-spec-creator|specline-spec-reviewer|specline-frontend-dev|specline-backend-dev|specline-code-reviewer|specline-test-writer|specline-test-runner"
2
+ # specline-agent-guard.sh — subagentStart Hook(增强版)
3
+ # 白名单校验 + 流水线阶段匹配校验
4
+ #
5
+ # Input (stdin JSON):
6
+ # { "subagent_type": "...", "subagent_id": "...", "task": "...", ... }
7
+ #
8
+ # Output (stdout JSON):
9
+ # { "permission": "allow" } 或 { "permission": "deny", "user_message": "..." }
10
+
11
+ set -euo pipefail
12
+
4
13
  input=$(cat)
5
14
  subagent_type=$(echo "$input" | jq -r '.subagent_type // empty')
6
- if [ -z "$subagent_type" ]; then
15
+
16
+ # 非 specline agent → 放行(不受 Specline 管控)
17
+ if ! echo "$subagent_type" | grep -qE "^specline-"; then
7
18
  echo '{"permission": "allow"}'
8
19
  exit 0
9
20
  fi
10
- if echo "$subagent_type" | grep -qE "^($ALLOWED_AGENTS)$"; then
21
+
22
+ # ===== 1. 白名单校验 =====
23
+ ALLOWED_AGENTS="specline-spec-creator|specline-spec-reviewer|specline-frontend-dev|specline-backend-dev|specline-config-dev|specline-code-reviewer|specline-config-reviewer|specline-test-writer|specline-test-runner"
24
+
25
+ if ! echo "$subagent_type" | grep -qE "^($ALLOWED_AGENTS)$"; then
26
+ echo "{\"permission\": \"deny\", \"user_message\": \"子Agent类型 '$subagent_type' 不在 Specline 允许列表中。允许的类型: spec-creator, spec-reviewer, frontend-dev, backend-dev, config-dev, code-reviewer, config-reviewer, test-writer, test-runner\"}"
27
+ exit 0
28
+ fi
29
+
30
+ # ===== 2. 流水线阶段匹配校验 =====
31
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
32
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
33
+ CHANGES_DIR="$PROJECT_ROOT/specline/changes"
34
+
35
+ # 查找活跃 pipeline
36
+ find_active_pipeline() {
37
+ if [ ! -d "$CHANGES_DIR" ]; then
38
+ echo ""
39
+ return
40
+ 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
+
54
+ STATE_FILE=$(find_active_pipeline)
55
+
56
+ # 无活跃 pipeline → 放行(不在流水线中的会话可以使用任何 specline agent)
57
+ if [ -z "$STATE_FILE" ]; then
11
58
  echo '{"permission": "allow"}'
12
59
  exit 0
13
60
  fi
14
- echo "{\"permission\": \"deny\", \"user_message\": \"子Agent类型 '$subagent_type' 不在允许列表中。\"}"
61
+
62
+ phase=$(jq -r '.current_phase' "$STATE_FILE")
63
+ change=$(jq -r '.change_name' "$STATE_FILE")
64
+
65
+ # 判断 Agent 类型分类
66
+ is_spec_agent=$(echo "$subagent_type" | grep -qE "specline-spec-creator|specline-spec-reviewer" && echo "true" || echo "false")
67
+ is_coding_agent=$(echo "$subagent_type" | grep -qE "^(specline-frontend-dev|specline-backend-dev|specline-config-dev)$" && echo "true" || echo "false")
68
+ is_review_agent=$(echo "$subagent_type" | grep -qE "^(specline-code-reviewer|specline-config-reviewer)$" && echo "true" || echo "false")
69
+ is_test_agent=$(echo "$subagent_type" | grep -qE "specline-test-writer|specline-test-runner" && echo "true" || echo "false")
70
+
71
+ case "$phase" in
72
+ spec)
73
+ if [ "$is_coding_agent" = "true" ]; then
74
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动编码 Agent: $subagent_type。变更: $change。请先完成 SPEC → CODING 阶段切换。\"}"
75
+ exit 0
76
+ fi
77
+ if [ "$is_test_agent" = "true" ]; then
78
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动测试 Agent: $subagent_type。变更: $change。\"}"
79
+ exit 0
80
+ fi
81
+ if [ "$is_review_agent" = "true" ]; then
82
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动代码审查 Agent: $subagent_type。变更: $change。请使用 specline-spec-reviewer。\"}"
83
+ exit 0
84
+ fi
85
+ # spec-creator, spec-reviewer → 放行
86
+ ;;
87
+
88
+ coding)
89
+ if [ "$is_spec_agent" = "true" ]; then
90
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODING 阶段不能启动 Spec Agent: $subagent_type。变更: $change。如需修改 Spec,请手动编辑文件。\"}"
91
+ exit 0
92
+ fi
93
+ # 编码/测试/审查 agent → 放行
94
+ ;;
95
+
96
+ code_review)
97
+ if [ "$is_spec_agent" = "true" ]; then
98
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODE REVIEW 阶段不能启动 Spec Agent: $subagent_type。变更: $change。\"}"
99
+ exit 0
100
+ fi
101
+ if [ "$is_test_agent" = "true" ] && [ "$subagent_type" != "specline-test-writer" ]; then
102
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODE REVIEW 阶段不能启动测试运行 Agent。变更: $change。Test 在下一阶段。\"}"
103
+ exit 0
104
+ fi
105
+ ;;
106
+
107
+ test)
108
+ if [ "$is_spec_agent" = "true" ]; then
109
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 TEST 阶段不能启动 Spec Agent: $subagent_type。变更: $change。\"}"
110
+ exit 0
111
+ fi
112
+ # 编码/测试/审查 agent → 放行(测试阶段可能需要编码修复)
113
+ ;;
114
+
115
+ archive)
116
+ echo "{\"permission\": \"deny\", \"user_message\": \"🚫 变更 $change 已归档,不能启动任何子 Agent。\"}"
117
+ exit 0
118
+ ;;
119
+ esac
120
+
121
+ # 阶段匹配 + 白名单都通过
122
+ echo '{"permission": "allow"}'
15
123
  exit 0
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env bash
2
+ # specline-phase-guard.sh — preToolUse Hook
3
+ # 操作前按流水线阶段校验并拦截违规行为
4
+ #
5
+ # Input (stdin JSON):
6
+ # { "tool_name": "Write"|"Task"|"Shell"|"Delete"|..., "tool_input": {...}, ... }
7
+ #
8
+ # Output (stdout JSON):
9
+ # { "permission": "allow" } 或 { "permission": "deny", "user_message": "...", "agent_message": "..." }
10
+
11
+ set -euo pipefail
12
+
13
+ input=$(cat)
14
+ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
15
+ tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19
+ CHANGES_DIR="$PROJECT_ROOT/specline/changes"
20
+
21
+ # ===== 辅助函数 =====
22
+
23
+ # 查找第一个活跃 pipeline(排除 archive/)
24
+ find_active_pipeline() {
25
+ if [ ! -d "$CHANGES_DIR" ]; then
26
+ echo ""
27
+ return
28
+ fi
29
+ for f in "$CHANGES_DIR"/*/.pipeline-state.json; do
30
+ [ -f "$f" ] || continue
31
+ if echo "$f" | grep -q "/archive/"; then continue; fi
32
+ local ph
33
+ ph=$(jq -r '.current_phase // ""' "$f" 2>/dev/null)
34
+ if [ "$ph" != "archive" ] && [ "$ph" != "" ]; then
35
+ echo "$f"
36
+ return
37
+ fi
38
+ done
39
+ echo ""
40
+ }
41
+
42
+ deny() {
43
+ local user_msg="$1"
44
+ local agent_msg="$2"
45
+ # JSON 转义
46
+ user_msg=$(echo "$user_msg" | jq -Rs '.')
47
+ agent_msg=$(echo "$agent_msg" | jq -Rs '.')
48
+ cat << EOF
49
+ {
50
+ "permission": "deny",
51
+ "user_message": ${user_msg},
52
+ "agent_message": ${agent_msg}
53
+ }
54
+ EOF
55
+ }
56
+
57
+ allow() {
58
+ echo '{"permission": "allow"}'
59
+ }
60
+
61
+ # ===== 主逻辑 =====
62
+
63
+ STATE_FILE=$(find_active_pipeline)
64
+
65
+ # 无活跃 pipeline → 透明放行
66
+ if [ -z "$STATE_FILE" ]; then
67
+ allow
68
+ exit 0
69
+ fi
70
+
71
+ phase=$(jq -r '.current_phase' "$STATE_FILE")
72
+ change=$(jq -r '.change_name' "$STATE_FILE")
73
+ tasks_file="$CHANGES_DIR/$change/tasks.md"
74
+
75
+ # ---- Write / StrReplace / EditNotebook ----
76
+ case "$tool_name" in
77
+ Write|StrReplace|EditNotebook)
78
+ # SPEC 阶段:禁止编辑非 specline 的源代码文件
79
+ if [ "$phase" = "spec" ]; then
80
+ file_path=$(echo "$tool_input" | jq -r '.path // .file_path // empty')
81
+
82
+ if [ -n "$file_path" ]; then
83
+ # 允许 specline 规划文件
84
+ if echo "$file_path" | grep -q "specline/"; then
85
+ allow
86
+ exit 0
87
+ fi
88
+
89
+ # 允许 hook/config 文件(流水线基础设施)
90
+ if echo "$file_path" | grep -qE "(\.cursor/|hooks\.json|package\.json)"; then
91
+ allow
92
+ exit 0
93
+ fi
94
+
95
+ # 代码文件 → 拦截
96
+ if echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|css|html|vue|svelte)$'; then
97
+ deny \
98
+ "🚫 SPEC 阶段不能编辑代码文件: $(basename "$file_path")" \
99
+ "当前处于 SPEC 阶段(变更:$change)。不能直接编辑应用代码文件。只有在进入 CODING 阶段后才能修改代码。如需修改规划文件,只能编辑 specline/changes/ 下的内容。"
100
+ exit 0
101
+ fi
102
+ fi
103
+ fi
104
+
105
+ # ARCHIVE 阶段:禁止任何编辑
106
+ if [ "$phase" = "archive" ]; then
107
+ deny \
108
+ "🚫 变更 $change 已归档,不能修改文件" \
109
+ "变更 $change 已归档。不能修改任何文件。如需继续工作,请创建新的变更。"
110
+ exit 0
111
+ fi
112
+
113
+ # CODING/CODE_REVIEW/TEST 阶段:放行
114
+ # (子 Agent 需要编辑代码文件,Orchestrator 的行为靠 SKILL + sessionStart 上下文约束)
115
+ allow
116
+ ;;
117
+
118
+ # ---- Task (子Agent) ----
119
+ Task)
120
+ subagent_type=$(echo "$tool_input" | jq -r '.subagent_type // empty')
121
+
122
+ # 不是 specline agent → 放行
123
+ if ! echo "$subagent_type" | grep -qE "^specline-"; then
124
+ allow
125
+ exit 0
126
+ fi
127
+
128
+ is_coding_agent=$(echo "$subagent_type" | grep -qE "^(specline-frontend-dev|specline-backend-dev|specline-config-dev)$" && echo "true" || echo "false")
129
+ is_spec_agent=$(echo "$subagent_type" | grep -qE "^(specline-spec-creator|specline-spec-reviewer)$" && echo "true" || echo "false")
130
+ is_test_agent=$(echo "$subagent_type" | grep -qE "^(specline-test-writer|specline-test-runner)$" && echo "true" || echo "false")
131
+ is_review_agent=$(echo "$subagent_type" | grep -qE "^(specline-code-reviewer|specline-config-reviewer)$" && echo "true" || echo "false")
132
+
133
+ case "$phase" in
134
+ spec)
135
+ if [ "$is_coding_agent" = "true" ]; then
136
+ deny \
137
+ "🚫 SPEC 阶段不能启动编码 Agent ($subagent_type)" \
138
+ "当前在 SPEC 阶段(变更:$change)。编码 Agent($subagent_type)只能在 CODING 阶段启动。如需编码,请先完成 Spec Gate 和人工确认。"
139
+ exit 0
140
+ fi
141
+ if [ "$is_test_agent" = "true" ]; then
142
+ deny \
143
+ "🚫 SPEC 阶段不能启动测试 Agent ($subagent_type)" \
144
+ "当前在 SPEC 阶段(变更:$change)。测试 Agent 只能在 CODING 或 TEST 阶段启动。"
145
+ exit 0
146
+ fi
147
+ # spec-creator / spec-reviewer → 放行
148
+ ;;
149
+
150
+ coding)
151
+ if [ "$is_spec_agent" = "true" ]; then
152
+ deny \
153
+ "🚫 CODING 阶段不能启动 Spec Agent ($subagent_type)" \
154
+ "当前在 CODING 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。如需修改 Spec 文档,请手动编辑 specline/changes/ 下的文件后恢复流水线。"
155
+ exit 0
156
+ fi
157
+ # 编码/测试/审查 agent → 放行
158
+ ;;
159
+
160
+ code_review)
161
+ if [ "$is_spec_agent" = "true" ]; then
162
+ deny \
163
+ "🚫 CODE REVIEW 阶段不能启动 Spec Agent ($subagent_type)" \
164
+ "当前在 CODE REVIEW 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。"
165
+ exit 0
166
+ fi
167
+ ;;
168
+
169
+ test)
170
+ if [ "$is_spec_agent" = "true" ]; then
171
+ deny \
172
+ "🚫 TEST 阶段不能启动 Spec Agent ($subagent_type)" \
173
+ "当前在 TEST 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。"
174
+ exit 0
175
+ fi
176
+ ;;
177
+ esac
178
+
179
+ allow
180
+ ;;
181
+
182
+ # ---- Delete ----
183
+ Delete)
184
+ # SPEC 阶段禁止删除代码文件
185
+ if [ "$phase" = "spec" ]; then
186
+ file_path=$(echo "$tool_input" | jq -r '.path // empty')
187
+ if [ -n "$file_path" ] && ! echo "$file_path" | grep -q "specline/"; then
188
+ deny \
189
+ "🚫 SPEC 阶段不能删除代码文件" \
190
+ "当前在 SPEC 阶段,不能删除应用代码文件。"
191
+ exit 0
192
+ fi
193
+ fi
194
+ allow
195
+ ;;
196
+
197
+ *)
198
+ allow
199
+ ;;
200
+ esac
201
+
202
+ exit 0
@@ -88,6 +88,7 @@ gate_new() {
88
88
  fi
89
89
 
90
90
  mkdir -p "$change_dir/specs"
91
+ mkdir -p "$change_dir/.tmp"
91
92
 
92
93
  # 写入 .specline.yaml
93
94
  cat > "$change_dir/.specline.yaml" << YAML
@@ -363,8 +364,8 @@ gate_lint() {
363
364
  fi
364
365
  fi
365
366
 
366
- # code-review.json error 计数
367
- local review_file="$PROJECT_ROOT/code-review.json"
367
+ # code-review.json error 计数(位于 change 的 .tmp/ 目录下)
368
+ local review_file="$PROJECT_ROOT/specline/changes/$CHANGE/.tmp/code-review.json"
368
369
  if [ -f "$review_file" ]; then
369
370
  local error_count
370
371
  error_count=$(jq '[.findings[] | select(.severity=="error")] | length' "$review_file" 2>/dev/null || echo "0")
@@ -584,6 +585,8 @@ gate_archive() {
584
585
  mv "$src_dir" "$dest"
585
586
  echo "✅ 已归档到: $dest"
586
587
 
588
+ # 临时文件(.tmp/)随 change 目录一起归档,无需单独清理
589
+
587
590
  # 更新状态文件
588
591
  if [ -f "$dest/.pipeline-state.json" ]; then
589
592
  local now
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # specline-reminder.sh — postToolUse Hook
3
+ # 关键操作后注入下一步提醒到 Agent 上下文
4
+ #
5
+ # Input (stdin JSON):
6
+ # { "tool_name": "Task"|"Write"|..., "tool_input": {...}, "tool_output": "...", ... }
7
+ #
8
+ # Output (stdout JSON):
9
+ # { "additional_context": "<提醒文本>" } 或 {}
10
+
11
+ set -euo pipefail
12
+
13
+ input=$(cat)
14
+ tool_name=$(echo "$input" | jq -r '.tool_name // empty')
15
+ tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
19
+ CHANGES_DIR="$PROJECT_ROOT/specline/changes"
20
+
21
+ # ===== 辅助函数 =====
22
+
23
+ find_active_pipeline() {
24
+ if [ ! -d "$CHANGES_DIR" ]; then
25
+ echo ""
26
+ return
27
+ 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
+
41
+ STATE_FILE=$(find_active_pipeline)
42
+
43
+ # 无活跃 pipeline → 透明
44
+ if [ -z "$STATE_FILE" ]; then
45
+ echo '{}'
46
+ exit 0
47
+ fi
48
+
49
+ phase=$(jq -r '.current_phase' "$STATE_FILE")
50
+ change=$(jq -r '.change_name' "$STATE_FILE")
51
+
52
+ # ---- Task (子Agent) 完成后 ----
53
+ if [ "$tool_name" = "Task" ]; then
54
+ subagent_type=$(echo "$tool_input" | jq -r '.subagent_type // ""')
55
+
56
+ if [ "$phase" = "coding" ] && echo "$subagent_type" | grep -qE "^specline-"; then
57
+ # 检查是否还有同批次未完成的任务
58
+ pending_in_batch="?"
59
+ pending_tasks=$(jq -r '[.phases.coding.tasks[]? | select(.status == "pending")] | length' "$STATE_FILE" 2>/dev/null || echo "0")
60
+
61
+ reminder="🔔 **[Pipeline] 子 Agent ($subagent_type) 已完成**\n"
62
+ reminder+="- 变更: $change | 阶段: CODING\n"
63
+
64
+ if [ "$pending_tasks" != "0" ] && [ "$pending_tasks" != "?" ]; then
65
+ reminder+="- 还有 ${pending_tasks} 个任务未完成\n"
66
+ fi
67
+
68
+ reminder+="- ⚠️ 请更新 tasks.md 中对应任务的 checkbox:[ ] → [x]\n"
69
+ reminder+="- ⚠️ 当前批次全部完成后,运行 Build Gate:\n"
70
+ reminder+=" \`.cursor/hooks/specline-pipeline-gate.sh build --change \"$change\"\`"
71
+
72
+ escaped=$(echo -e "$reminder" | jq -Rs '.')
73
+ cat << EOF
74
+ {
75
+ "additional_context": ${escaped}
76
+ }
77
+ EOF
78
+ exit 0
79
+ fi
80
+
81
+ if [ "$phase" = "test" ] && echo "$subagent_type" | grep -qE "^specline-"; then
82
+ reminder="🔔 **[Pipeline] 测试修复 Agent ($subagent_type) 已完成**\n"
83
+ reminder+="- 变更: $change | 阶段: TEST\n"
84
+ reminder+="- ⚠️ 请重新运行对应的测试 Gate:\n"
85
+ reminder+=" \`.cursor/hooks/specline-pipeline-gate.sh test-unit --change \"$change\"\`"
86
+
87
+ escaped=$(echo -e "$reminder" | jq -Rs '.')
88
+ cat << EOF
89
+ {
90
+ "additional_context": ${escaped}
91
+ }
92
+ EOF
93
+ exit 0
94
+ fi
95
+ fi
96
+
97
+ # ---- Write / StrReplace 完成后 ----
98
+ if [ "$tool_name" = "Write" ] || [ "$tool_name" = "StrReplace" ]; then
99
+ file_path=$(echo "$tool_input" | jq -r '.path // .file_path // empty')
100
+
101
+ if [ "$phase" = "coding" ] && [ -n "$file_path" ]; then
102
+ # 编辑 tasks.md → 不提醒(这是正确的行为)
103
+ if echo "$file_path" | grep -q "tasks.md"; then
104
+ echo '{}'
105
+ exit 0
106
+ fi
107
+
108
+ # 编辑代码文件 → 提醒(Orchestrator 不应该直接编辑)
109
+ if echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx|py|go|rs)$'; then
110
+ reminder="🔔 **[Pipeline] 检测到直接编辑代码文件**\n"
111
+ reminder+="- 文件: $(basename "$file_path")\n"
112
+ reminder+="- 编码应通过子 Agent 完成,而非直接编辑\n"
113
+ reminder+="- 如果这是 Orchestrator 的错误行为,请改用 Task 工具启动对应子 Agent"
114
+
115
+ escaped=$(echo -e "$reminder" | jq -Rs '.')
116
+ cat << EOF
117
+ {
118
+ "additional_context": ${escaped}
119
+ }
120
+ EOF
121
+ exit 0
122
+ fi
123
+ fi
124
+ fi
125
+
126
+ # ---- Shell 执行后检查 Gate ----
127
+ if [ "$tool_name" = "Shell" ]; then
128
+ command=$(echo "$tool_input" | jq -r '.command // empty')
129
+
130
+ # 如果是执行 gate 脚本 → 不提醒
131
+ if echo "$command" | grep -q "specline-pipeline-gate.sh"; then
132
+ echo '{}'
133
+ exit 0
134
+ fi
135
+ fi
136
+
137
+ # 默认:无提醒
138
+ echo '{}'
139
+ exit 0