specline 2.0.0 → 2.0.2
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/core/agents/specline-spec-creator.yaml +16 -0
- package/core/agents/specline-spec-reviewer.yaml +14 -2
- package/core/skills/specline-pipeline/SKILL.md +63 -9
- package/lib/merge.mjs +7 -3
- package/package.json +1 -1
- package/templates/.cursor/README.md +0 -18
- package/templates/.cursor/agents/specline-backend-dev.md +0 -47
- package/templates/.cursor/agents/specline-code-reviewer.md +0 -110
- package/templates/.cursor/agents/specline-config-dev.md +0 -52
- package/templates/.cursor/agents/specline-config-reviewer.md +0 -79
- package/templates/.cursor/agents/specline-explore-assistant.md +0 -81
- package/templates/.cursor/agents/specline-frontend-dev.md +0 -47
- package/templates/.cursor/agents/specline-spec-creator.md +0 -376
- package/templates/.cursor/agents/specline-spec-reviewer.md +0 -144
- package/templates/.cursor/agents/specline-test-runner.md +0 -107
- package/templates/.cursor/agents/specline-test-writer.md +0 -170
- package/templates/.cursor/hooks/specline-agent-guard.sh +0 -131
- package/templates/.cursor/hooks/specline-auto-format.sh +0 -12
- package/templates/.cursor/hooks/specline-phase-guard.sh +0 -201
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a1-covers-ref.sh +0 -125
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +0 -171
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c1-exception.sh +0 -71
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/c2-vague.sh +0 -60
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/common.sh +0 -68
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d1-cycle.sh +0 -149
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/d3-type-file.sh +0 -260
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +0 -1569
- package/templates/.cursor/hooks/specline-reminder.sh +0 -147
- package/templates/.cursor/hooks/specline-session-start.sh +0 -259
- package/templates/.cursor/hooks/specline-shell-guard.sh +0 -18
- package/templates/.cursor/hooks.json +0 -46
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +0 -197
- package/templates/.cursor/skills/specline-archive-change/SKILL.md +0 -173
- package/templates/.cursor/skills/specline-explore/SKILL.md +0 -504
- package/templates/.cursor/skills/specline-knowledge/SKILL.md +0 -539
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +0 -616
- package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +0 -49
- package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +0 -59
- package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +0 -87
- package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +0 -253
- package/templates/.cursor/skills/specline-propose/SKILL.md +0 -186
- package/templates/.cursor/skills/specline-quickfix/SKILL.md +0 -265
- package/templates/specline/config.yaml +0 -64
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: specline-test-writer
|
|
3
|
-
description: 黑盒测试工程师——只能基于 Spec 文档编写测试,不能读取任何实现源代码。语言无关,自动检测项目测试框架。确保测试用例完全从需求而非实现角度设计。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
你是**黑盒测试工程师**。你的工作原则是语言无关的,适配任何技术栈。
|
|
7
|
-
|
|
8
|
-
## 语言与框架检测(写测试前必须先做)
|
|
9
|
-
|
|
10
|
-
在编写任何测试代码之前,先检测项目的技术栈和测试框架:
|
|
11
|
-
|
|
12
|
-
### 1. 读取项目配置文件,确定语言和框架
|
|
13
|
-
|
|
14
|
-
| 配置文件 | 推断语言 | 测试框架 | 测试目录 |
|
|
15
|
-
|---------|---------|---------|---------|
|
|
16
|
-
| `package.json` 含 jest | TypeScript/JavaScript | **Jest** | `__tests__/`, `*.test.ts` |
|
|
17
|
-
| `package.json` 含 vitest | TypeScript/JavaScript | **Vitest** | `__tests__/`, `*.test.ts` |
|
|
18
|
-
| `package.json` 含 mocha | TypeScript/JavaScript | **Mocha + Chai** | `test/` |
|
|
19
|
-
| `pyproject.toml` / `setup.cfg` | Python | **pytest** | `tests/` |
|
|
20
|
-
| `go.mod` | Go | **go test** | `*_test.go` |
|
|
21
|
-
| `Cargo.toml` | Rust | **cargo test** | `tests/`, `#[cfg(test)]` |
|
|
22
|
-
| `pom.xml` / `build.gradle` | Java/Kotlin | **JUnit 5** | `src/test/` |
|
|
23
|
-
| `Gemfile` | Ruby | **RSpec** | `spec/` |
|
|
24
|
-
| `mix.exs` | Elixir | **ExUnit** | `test/` |
|
|
25
|
-
|
|
26
|
-
### 2. 确定测试文件路径和命名规范
|
|
27
|
-
|
|
28
|
-
根据检测到的框架,确定:
|
|
29
|
-
- 测试文件放在哪个目录
|
|
30
|
-
- 测试文件/函数的命名规范
|
|
31
|
-
- 断言库/方法
|
|
32
|
-
|
|
33
|
-
### 3. 如无法检测到任何测试框架,默认使用最简方案
|
|
34
|
-
|
|
35
|
-
按语言默认映射:
|
|
36
|
-
- JS/TS → Jest
|
|
37
|
-
- Python → pytest
|
|
38
|
-
- Go → go test
|
|
39
|
-
- Rust → cargo test
|
|
40
|
-
|
|
41
|
-
写入状态时记录检测结果:`"test_framework": "jest"`
|
|
42
|
-
|
|
43
|
-
## 核心约束(必须严格遵守)
|
|
44
|
-
|
|
45
|
-
1. **不能读取实现源代码**:禁止读取任何业务逻辑、组件实现、API handler 等源码文件
|
|
46
|
-
2. **只能基于以下输入**:
|
|
47
|
-
- Spec 文档(需求规格,获取行为验收标准 WHEN/THEN)
|
|
48
|
-
- `design.md` 的「对外接口契约」章节(获取 CLI 命令、HTTP 端点、模块导出的技术签名——如果章节不存在则跳过)
|
|
49
|
-
- `tasks.md`(Covers 追溯链)
|
|
50
|
-
- 项目的 `package.json`/`pyproject.toml` 等**配置文件**(用于确定框架,不是实现代码)
|
|
51
|
-
3. **只能通过 CLI 执行或 HTTP 调用来验证行为**,不可直接 import 内部模块或组件
|
|
52
|
-
|
|
53
|
-
## 工作方式
|
|
54
|
-
|
|
55
|
-
1. 检测项目技术栈,确定测试框架
|
|
56
|
-
2. 仔细阅读 Spec 中的每个 Scenario,理解验收标准(WHEN/THEN)
|
|
57
|
-
3. **读取 `design.md` 的「对外接口契约」章节**,获取 CLI 命令格式、HTTP 端点、模块导出签名
|
|
58
|
-
- 如果 design.md 无此章节 → 此 change 无黑盒测试任务需求,技能级自检:确认 tasks.md 末尾「测试文件归属」表格中是否确实无 specline-test-writer 负责的测试任务
|
|
59
|
-
4. 对照 `tasks.md` 中的 `Covers` 追溯链,确保每个 Scenario 都有测试覆盖
|
|
60
|
-
5. 每个 Scenario 至少生成 1 个对应的测试函数
|
|
61
|
-
6. 测试函数命名遵循对应框架的约定
|
|
62
|
-
7. 测试函数必须包含描述性注释/名称(对应 Spec 中的 Scenario 名称)
|
|
63
|
-
|
|
64
|
-
## 合同驱动测试 (Contract-Driven Testing)
|
|
65
|
-
|
|
66
|
-
当 design.md 包含「对外接口契约」章节时,按以下规则编写测试:
|
|
67
|
-
|
|
68
|
-
### CLI 命令测试
|
|
69
|
-
|
|
70
|
-
从契约的 CLI 命令表格获取命令格式和参数:
|
|
71
|
-
```python
|
|
72
|
-
# 契约: | quickfix | `specline quickfix <description>` | description: string | stdout: summary, exit 0/1 |
|
|
73
|
-
def test_quickfix_success():
|
|
74
|
-
result = subprocess.run(["specline", "quickfix", "修复按钮"], capture_output=True, text=True)
|
|
75
|
-
assert result.returncode == 0
|
|
76
|
-
assert "summary" in result.stdout.lower()
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### HTTP 端点测试
|
|
80
|
-
|
|
81
|
-
从契约的 HTTP 端点表格获取路径和方法:
|
|
82
|
-
```python
|
|
83
|
-
# 契约: | POST | /api/users | `{ name: string, email: string }` | 201 + `{ id: string }` | 409 `{ "error": "duplicate" }` |
|
|
84
|
-
def test_create_user_success():
|
|
85
|
-
resp = requests.post(f"{BASE_URL}/api/users", json={"name": "张三", "email": "test@example.com"})
|
|
86
|
-
assert resp.status_code == 201
|
|
87
|
-
assert "id" in resp.json()
|
|
88
|
-
|
|
89
|
-
def test_create_user_duplicate():
|
|
90
|
-
# 先创建一个用户
|
|
91
|
-
requests.post(f"{BASE_URL}/api/users", json={"name": "张三", "email": "dup@example.com"})
|
|
92
|
-
# 重复创建
|
|
93
|
-
resp = requests.post(f"{BASE_URL}/api/users", json={"name": "李四", "email": "dup@example.com"})
|
|
94
|
-
assert resp.status_code == 409
|
|
95
|
-
assert resp.json()["error"] == "duplicate"
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 模块导出测试
|
|
99
|
-
|
|
100
|
-
从契约的模块导出表格获取函数签名,但**只通过 CLI/HTTP 间接调用**,不直接 import:
|
|
101
|
-
```python
|
|
102
|
-
# 契约: | src/services/auth.py | createSession | `(userId: string) => Promise<Session>` |
|
|
103
|
-
# ❌ 不能: from src.services.auth import createSession
|
|
104
|
-
# ✅ 改为: 通过 HTTP API 间接测试
|
|
105
|
-
def test_create_session_via_api():
|
|
106
|
-
resp = requests.post(f"{BASE_URL}/api/sessions", json={"userId": "user-123"})
|
|
107
|
-
assert resp.status_code == 201
|
|
108
|
-
assert "sessionToken" in resp.json()
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**重要**:模块导出契约主要用于 Code Review 阶段校验实现一致性。test-writer 仍然只通过外部接口(CLI/HTTP)测试行为。
|
|
112
|
-
|
|
113
|
-
## 测试映射规则(语言无关)
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
Spec Scenario → Test Function
|
|
117
|
-
──────────────────────────────────── ──────────────────────────────────
|
|
118
|
-
#### Scenario: Successful login test("Successful login", () => { (Jest)
|
|
119
|
-
- **WHEN** user submits valid // WHEN 条件 → Arrange/Act
|
|
120
|
-
credentials const result = await login(...)
|
|
121
|
-
- **THEN** system returns JWT token // THEN 断言 → Assert
|
|
122
|
-
expect(result.token).toBeDefined()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#### Scenario: Invalid file def test_invalid_task_file(): (pytest)
|
|
126
|
-
- **WHEN** user runs CLI with # WHEN 条件 → Act
|
|
127
|
-
invalid task file result = run_cli("task run --file bad.json")
|
|
128
|
-
- **THEN** system exits with code 1 # THEN 断言 → Assert
|
|
129
|
-
assert result.returncode == 1
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#### Scenario: Data persistence func TestDataPersistence(t *testing.T) { (go test)
|
|
133
|
-
- **WHEN** server restarts // WHEN 条件 → Act
|
|
134
|
-
- **THEN** previously stored data // THEN 断言 → Assert
|
|
135
|
-
is still accessible
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
核心原则:**每个 Scenario 的 WHEN 转为准备/执行步骤,THEN 转为断言。**
|
|
139
|
-
|
|
140
|
-
## 禁止事项
|
|
141
|
-
|
|
142
|
-
- ❌ 直接 import 或 require 项目内部模块/组件
|
|
143
|
-
- ❌ 读取 `agent/`、`server/`、`src/`、`lib/` 等包含业务逻辑的目录下的源代码文件
|
|
144
|
-
- ❌ 直接调用内部函数、类、或数据库操作方法
|
|
145
|
-
- ❌ 绕过公开接口(CLI/HTTP API)直接访问内部状态
|
|
146
|
-
|
|
147
|
-
## 允许事项
|
|
148
|
-
|
|
149
|
-
- ✅ 读取项目配置文件(`package.json`、`pyproject.toml`、`go.mod` 等)
|
|
150
|
-
- ✅ 使用对应语言的 subprocess/shell 调用 CLI 命令
|
|
151
|
-
- ✅ 使用对应语言的 HTTP 客户端调用 API
|
|
152
|
-
- ✅ 创建临时文件和测试 fixture 数据
|
|
153
|
-
- ✅ 读取 Spec、Design、Tasks 等规划文档
|
|
154
|
-
|
|
155
|
-
## 产出报告
|
|
156
|
-
|
|
157
|
-
完成后输出 JSON 到 `specline/changes/<change>/.tmp/test-code-result.json`:
|
|
158
|
-
|
|
159
|
-
```json
|
|
160
|
-
{
|
|
161
|
-
"status": "completed",
|
|
162
|
-
"test_framework": "jest",
|
|
163
|
-
"language": "typescript",
|
|
164
|
-
"test_dir": "__tests__",
|
|
165
|
-
"files_created": ["__tests__/login.test.ts", "__tests__/api.test.ts"],
|
|
166
|
-
"scenarios_covered": 12,
|
|
167
|
-
"scenarios_total": 14,
|
|
168
|
-
"uncovered_scenarios": ["Scenario: 边缘情况X", "Scenario: 异常路径Y"]
|
|
169
|
-
}
|
|
170
|
-
```
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
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
|
-
|
|
13
|
-
input=$(cat)
|
|
14
|
-
subagent_type=$(echo "$input" | jq -r '.subagent_type // empty')
|
|
15
|
-
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
16
|
-
|
|
17
|
-
# 非 specline agent → 放行(不受 Specline 管控)
|
|
18
|
-
if ! echo "$subagent_type" | grep -qE "^specline-"; then
|
|
19
|
-
echo '{"permission": "allow"}'
|
|
20
|
-
exit 0
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
# ===== 1. 白名单校验 =====
|
|
24
|
-
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"
|
|
25
|
-
|
|
26
|
-
if ! echo "$subagent_type" | grep -qE "^($ALLOWED_AGENTS)$"; then
|
|
27
|
-
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\"}"
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# ===== 2. 流水线阶段匹配校验 =====
|
|
32
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
-
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
34
|
-
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
35
|
-
|
|
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
|
|
43
|
-
fi
|
|
44
|
-
|
|
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
|
-
}
|
|
63
|
-
|
|
64
|
-
STATE_FILE=""
|
|
65
|
-
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
66
|
-
echo '{"permission": "allow"}'
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
phase=$(jq -r '.current_phase' "$STATE_FILE")
|
|
71
|
-
change=$(jq -r '.change_name' "$STATE_FILE")
|
|
72
|
-
|
|
73
|
-
# 判断 Agent 类型分类
|
|
74
|
-
is_spec_agent=$(echo "$subagent_type" | grep -qE "specline-spec-creator|specline-spec-reviewer" && echo "true" || echo "false")
|
|
75
|
-
is_coding_agent=$(echo "$subagent_type" | grep -qE "^(specline-frontend-dev|specline-backend-dev|specline-config-dev)$" && echo "true" || echo "false")
|
|
76
|
-
is_review_agent=$(echo "$subagent_type" | grep -qE "^(specline-code-reviewer|specline-config-reviewer)$" && echo "true" || echo "false")
|
|
77
|
-
is_test_agent=$(echo "$subagent_type" | grep -qE "specline-test-writer|specline-test-runner" && echo "true" || echo "false")
|
|
78
|
-
|
|
79
|
-
case "$phase" in
|
|
80
|
-
spec)
|
|
81
|
-
if [ "$is_coding_agent" = "true" ]; then
|
|
82
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动编码 Agent: $subagent_type。变更: $change。请先完成 SPEC → CODING 阶段切换。\"}"
|
|
83
|
-
exit 0
|
|
84
|
-
fi
|
|
85
|
-
if [ "$is_test_agent" = "true" ]; then
|
|
86
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动测试 Agent: $subagent_type。变更: $change。\"}"
|
|
87
|
-
exit 0
|
|
88
|
-
fi
|
|
89
|
-
if [ "$is_review_agent" = "true" ]; then
|
|
90
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 SPEC 阶段不能启动代码审查 Agent: $subagent_type。变更: $change。请使用 specline-spec-reviewer。\"}"
|
|
91
|
-
exit 0
|
|
92
|
-
fi
|
|
93
|
-
# spec-creator, spec-reviewer → 放行
|
|
94
|
-
;;
|
|
95
|
-
|
|
96
|
-
coding)
|
|
97
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
98
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODING 阶段不能启动 Spec Agent: $subagent_type。变更: $change。如需修改 Spec,请手动编辑文件。\"}"
|
|
99
|
-
exit 0
|
|
100
|
-
fi
|
|
101
|
-
# 编码/测试/审查 agent → 放行
|
|
102
|
-
;;
|
|
103
|
-
|
|
104
|
-
code_review)
|
|
105
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
106
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODE REVIEW 阶段不能启动 Spec Agent: $subagent_type。变更: $change。\"}"
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
if [ "$is_test_agent" = "true" ] && [ "$subagent_type" != "specline-test-writer" ]; then
|
|
110
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 CODE REVIEW 阶段不能启动测试运行 Agent。变更: $change。Test 在下一阶段。\"}"
|
|
111
|
-
exit 0
|
|
112
|
-
fi
|
|
113
|
-
;;
|
|
114
|
-
|
|
115
|
-
test)
|
|
116
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
117
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 TEST 阶段不能启动 Spec Agent: $subagent_type。变更: $change。\"}"
|
|
118
|
-
exit 0
|
|
119
|
-
fi
|
|
120
|
-
# 编码/测试/审查 agent → 放行(测试阶段可能需要编码修复)
|
|
121
|
-
;;
|
|
122
|
-
|
|
123
|
-
archive)
|
|
124
|
-
echo "{\"permission\": \"deny\", \"user_message\": \"🚫 变更 $change 已归档,不能启动任何子 Agent。\"}"
|
|
125
|
-
exit 0
|
|
126
|
-
;;
|
|
127
|
-
esac
|
|
128
|
-
|
|
129
|
-
# 阶段匹配 + 白名单都通过
|
|
130
|
-
echo '{"permission": "allow"}'
|
|
131
|
-
exit 0
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# auto-format.sh — afterFileEdit Hook: 自动格式化
|
|
3
|
-
input=$(cat)
|
|
4
|
-
filepath=$(echo "$input" | jq -r '.file // empty')
|
|
5
|
-
if [ -z "$filepath" ]; then exit 0; fi
|
|
6
|
-
if echo "$filepath" | grep -qE "\.py$"; then
|
|
7
|
-
command -v ruff &>/dev/null && ruff format "$filepath" 2>/dev/null || true
|
|
8
|
-
fi
|
|
9
|
-
if echo "$filepath" | grep -qE "\.(ts|tsx|js)$"; then
|
|
10
|
-
command -v npx &>/dev/null && npx prettier --write "$filepath" 2>/dev/null || true
|
|
11
|
-
fi
|
|
12
|
-
exit 0
|
|
@@ -1,201 +0,0 @@
|
|
|
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
|
-
SESSION_ID=$(echo "$input" | jq -r '.session_id // empty')
|
|
15
|
-
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
|
|
16
|
-
tool_input=$(echo "$input" | jq -r '.tool_input // "{}"')
|
|
17
|
-
|
|
18
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
-
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
20
|
-
CHANGES_DIR="$PROJECT_ROOT/specline/changes"
|
|
21
|
-
|
|
22
|
-
BINDINGS_FILE="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
23
|
-
[ ! -f "$BINDINGS_FILE" ] && echo '{}' > "$BINDINGS_FILE"
|
|
24
|
-
|
|
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
|
|
31
|
-
fi
|
|
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
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# ===== 辅助函数 =====
|
|
42
|
-
|
|
43
|
-
deny() {
|
|
44
|
-
local user_msg="$1"
|
|
45
|
-
local agent_msg="$2"
|
|
46
|
-
# JSON 转义
|
|
47
|
-
user_msg=$(echo "$user_msg" | jq -Rs '.')
|
|
48
|
-
agent_msg=$(echo "$agent_msg" | jq -Rs '.')
|
|
49
|
-
cat << EOF
|
|
50
|
-
{
|
|
51
|
-
"permission": "deny",
|
|
52
|
-
"user_message": ${user_msg},
|
|
53
|
-
"agent_message": ${agent_msg}
|
|
54
|
-
}
|
|
55
|
-
EOF
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
allow() {
|
|
59
|
-
echo '{"permission": "allow"}'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
# ===== 主逻辑 =====
|
|
63
|
-
|
|
64
|
-
STATE_FILE=""
|
|
65
|
-
if ! resolve_pipeline_for_session "$SESSION_ID"; then
|
|
66
|
-
allow # 无绑定 → 透明放行
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
phase=$(jq -r '.current_phase' "$STATE_FILE")
|
|
71
|
-
change=$(jq -r '.change_name' "$STATE_FILE")
|
|
72
|
-
tasks_file="$CHANGES_DIR/$change/tasks.md"
|
|
73
|
-
|
|
74
|
-
# ---- Write / StrReplace / EditNotebook ----
|
|
75
|
-
case "$tool_name" in
|
|
76
|
-
Write|StrReplace|EditNotebook)
|
|
77
|
-
# SPEC 阶段:禁止编辑非 specline 的源代码文件
|
|
78
|
-
if [ "$phase" = "spec" ]; then
|
|
79
|
-
file_path=$(echo "$tool_input" | jq -r '.path // .file_path // empty')
|
|
80
|
-
|
|
81
|
-
if [ -n "$file_path" ]; then
|
|
82
|
-
# 允许 specline 规划文件
|
|
83
|
-
if echo "$file_path" | grep -q "specline/"; then
|
|
84
|
-
allow
|
|
85
|
-
exit 0
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
# 允许 hook/config 文件(流水线基础设施)
|
|
89
|
-
if echo "$file_path" | grep -qE "(\.cursor/|hooks\.json|package\.json)"; then
|
|
90
|
-
allow
|
|
91
|
-
exit 0
|
|
92
|
-
fi
|
|
93
|
-
|
|
94
|
-
# 代码文件 → 拦截
|
|
95
|
-
if echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|css|html|vue|svelte)$'; then
|
|
96
|
-
deny \
|
|
97
|
-
"🚫 SPEC 阶段不能编辑代码文件: $(basename "$file_path")" \
|
|
98
|
-
"当前处于 SPEC 阶段(变更:$change)。不能直接编辑应用代码文件。只有在进入 CODING 阶段后才能修改代码。如需修改规划文件,只能编辑 specline/changes/ 下的内容。"
|
|
99
|
-
exit 0
|
|
100
|
-
fi
|
|
101
|
-
fi
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
# ARCHIVE 阶段:禁止任何编辑
|
|
105
|
-
if [ "$phase" = "archive" ]; then
|
|
106
|
-
deny \
|
|
107
|
-
"🚫 变更 $change 已归档,不能修改文件" \
|
|
108
|
-
"变更 $change 已归档。不能修改任何文件。如需继续工作,请创建新的变更。"
|
|
109
|
-
exit 0
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# CODING/CODE_REVIEW/TEST 阶段:放行
|
|
113
|
-
# (子 Agent 需要编辑代码文件,Orchestrator 的行为靠 SKILL + sessionStart 上下文约束)
|
|
114
|
-
allow
|
|
115
|
-
;;
|
|
116
|
-
|
|
117
|
-
# ---- Task (子Agent) ----
|
|
118
|
-
Task)
|
|
119
|
-
subagent_type=$(echo "$tool_input" | jq -r '.subagent_type // empty')
|
|
120
|
-
|
|
121
|
-
# 不是 specline agent → 放行
|
|
122
|
-
if ! echo "$subagent_type" | grep -qE "^specline-"; then
|
|
123
|
-
allow
|
|
124
|
-
exit 0
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
is_coding_agent=$(echo "$subagent_type" | grep -qE "^(specline-frontend-dev|specline-backend-dev|specline-config-dev)$" && echo "true" || echo "false")
|
|
128
|
-
is_spec_agent=$(echo "$subagent_type" | grep -qE "^(specline-spec-creator|specline-spec-reviewer)$" && echo "true" || echo "false")
|
|
129
|
-
is_test_agent=$(echo "$subagent_type" | grep -qE "^(specline-test-writer|specline-test-runner)$" && echo "true" || echo "false")
|
|
130
|
-
is_review_agent=$(echo "$subagent_type" | grep -qE "^(specline-code-reviewer|specline-config-reviewer)$" && echo "true" || echo "false")
|
|
131
|
-
|
|
132
|
-
case "$phase" in
|
|
133
|
-
spec)
|
|
134
|
-
if [ "$is_coding_agent" = "true" ]; then
|
|
135
|
-
deny \
|
|
136
|
-
"🚫 SPEC 阶段不能启动编码 Agent ($subagent_type)" \
|
|
137
|
-
"当前在 SPEC 阶段(变更:$change)。编码 Agent($subagent_type)只能在 CODING 阶段启动。如需编码,请先完成 Spec Gate 和人工确认。"
|
|
138
|
-
exit 0
|
|
139
|
-
fi
|
|
140
|
-
if [ "$is_test_agent" = "true" ]; then
|
|
141
|
-
deny \
|
|
142
|
-
"🚫 SPEC 阶段不能启动测试 Agent ($subagent_type)" \
|
|
143
|
-
"当前在 SPEC 阶段(变更:$change)。测试 Agent 只能在 CODING 或 TEST 阶段启动。"
|
|
144
|
-
exit 0
|
|
145
|
-
fi
|
|
146
|
-
# spec-creator / spec-reviewer → 放行
|
|
147
|
-
;;
|
|
148
|
-
|
|
149
|
-
coding)
|
|
150
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
151
|
-
deny \
|
|
152
|
-
"🚫 CODING 阶段不能启动 Spec Agent ($subagent_type)" \
|
|
153
|
-
"当前在 CODING 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。如需修改 Spec 文档,请手动编辑 specline/changes/ 下的文件后恢复流水线。"
|
|
154
|
-
exit 0
|
|
155
|
-
fi
|
|
156
|
-
# 编码/测试/审查 agent → 放行
|
|
157
|
-
;;
|
|
158
|
-
|
|
159
|
-
code_review)
|
|
160
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
161
|
-
deny \
|
|
162
|
-
"🚫 CODE REVIEW 阶段不能启动 Spec Agent ($subagent_type)" \
|
|
163
|
-
"当前在 CODE REVIEW 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。"
|
|
164
|
-
exit 0
|
|
165
|
-
fi
|
|
166
|
-
;;
|
|
167
|
-
|
|
168
|
-
test)
|
|
169
|
-
if [ "$is_spec_agent" = "true" ]; then
|
|
170
|
-
deny \
|
|
171
|
-
"🚫 TEST 阶段不能启动 Spec Agent ($subagent_type)" \
|
|
172
|
-
"当前在 TEST 阶段(变更:$change)。Spec Agent 只能在 SPEC 阶段启动。"
|
|
173
|
-
exit 0
|
|
174
|
-
fi
|
|
175
|
-
;;
|
|
176
|
-
esac
|
|
177
|
-
|
|
178
|
-
allow
|
|
179
|
-
;;
|
|
180
|
-
|
|
181
|
-
# ---- Delete ----
|
|
182
|
-
Delete)
|
|
183
|
-
# SPEC 阶段禁止删除代码文件
|
|
184
|
-
if [ "$phase" = "spec" ]; then
|
|
185
|
-
file_path=$(echo "$tool_input" | jq -r '.path // empty')
|
|
186
|
-
if [ -n "$file_path" ] && ! echo "$file_path" | grep -q "specline/"; then
|
|
187
|
-
deny \
|
|
188
|
-
"🚫 SPEC 阶段不能删除代码文件" \
|
|
189
|
-
"当前在 SPEC 阶段,不能删除应用代码文件。"
|
|
190
|
-
exit 0
|
|
191
|
-
fi
|
|
192
|
-
fi
|
|
193
|
-
allow
|
|
194
|
-
;;
|
|
195
|
-
|
|
196
|
-
*)
|
|
197
|
-
allow
|
|
198
|
-
;;
|
|
199
|
-
esac
|
|
200
|
-
|
|
201
|
-
exit 0
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# a1-covers-ref.sh — A1: Covers 引用存在性验证
|
|
4
|
-
#
|
|
5
|
-
# 验证 tasks.md 中每个任务的 Covers 字段引用的 Requirement 名称和 Scenario
|
|
6
|
-
# 名称在 spec.md 中实际存在。
|
|
7
|
-
#
|
|
8
|
-
# 兼容 bash 3.2+(macOS 默认版本),不使用关联数组(declare -A)。
|
|
9
|
-
#
|
|
10
|
-
# 依赖 common.sh 中定义的:
|
|
11
|
-
# - semantic_error(code, msg)
|
|
12
|
-
# - semantic_warn(code, msg)
|
|
13
|
-
# - semantic_info(code, msg)
|
|
14
|
-
# - SEMANTIC_ERRORS / SEMANTIC_WARNINGS / SEMANTIC_INFOS 全局计数器
|
|
15
|
-
#
|
|
16
|
-
# 环境变量:
|
|
17
|
-
# SPEC_FILE — spec.md 的路径
|
|
18
|
-
# TASKS_FILE — tasks.md 的路径
|
|
19
|
-
|
|
20
|
-
# 确保正确处理多字节 UTF-8 字符(中文 Scenario/Requirement 名称)
|
|
21
|
-
export LC_ALL="${LC_ALL:-zh_CN.UTF-8}"
|
|
22
|
-
|
|
23
|
-
run_a1_covers_ref() {
|
|
24
|
-
# ==== 输入校验 ====
|
|
25
|
-
if [ ! -f "${SPEC_FILE:-}" ]; then
|
|
26
|
-
semantic_error "A1" "spec.md 不存在: ${SPEC_FILE:-未设置}"
|
|
27
|
-
return
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
if [ ! -f "${TASKS_FILE:-}" ]; then
|
|
31
|
-
semantic_error "A1" "tasks.md 不存在: ${TASKS_FILE:-未设置}"
|
|
32
|
-
return
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# ==== 临时文件(存储 Requirement 和 Scenario 名称集合) ====
|
|
36
|
-
# 兼容 bash 3.2,不使用 declare -A 关联数组
|
|
37
|
-
local _req_file _scen_file
|
|
38
|
-
_req_file=$(mktemp) || { semantic_error "A1" "无法创建临时文件"; return; }
|
|
39
|
-
_scen_file=$(mktemp) || { rm -f "$_req_file"; semantic_error "A1" "无法创建临时文件"; return; }
|
|
40
|
-
|
|
41
|
-
# ==== 1. 从 spec.md 提取 Requirement 和 Scenario 名称 ====
|
|
42
|
-
local current_req="" scen_name=""
|
|
43
|
-
|
|
44
|
-
while IFS= read -r line; do
|
|
45
|
-
if [[ "$line" =~ ^###[[:space:]]+Requirement:[[:space:]]+(.+)$ ]]; then
|
|
46
|
-
current_req="${BASH_REMATCH[1]}"
|
|
47
|
-
current_req=$(echo "$current_req" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
48
|
-
echo "$current_req" >> "$_req_file"
|
|
49
|
-
elif [[ "$line" =~ ^####[[:space:]]+Scenario:[[:space:]]+(.+)$ ]]; then
|
|
50
|
-
scen_name="${BASH_REMATCH[1]}"
|
|
51
|
-
scen_name=$(echo "$scen_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
52
|
-
if [ -n "$current_req" ]; then
|
|
53
|
-
echo "${current_req}|${scen_name}" >> "$_scen_file"
|
|
54
|
-
fi
|
|
55
|
-
fi
|
|
56
|
-
done < "$SPEC_FILE"
|
|
57
|
-
|
|
58
|
-
# ==== 2. 从 tasks.md 解析 Covers 引用 ====
|
|
59
|
-
local task_num=0
|
|
60
|
-
local covers_content req_name scenarios_str split_list
|
|
61
|
-
covers_content=""; req_name=""; scenarios_str=""; split_list=""
|
|
62
|
-
|
|
63
|
-
while IFS= read -r line; do
|
|
64
|
-
# 追踪任务编号(从 "## N." 标题行)
|
|
65
|
-
if [[ "$line" =~ ^##[[:space:]]+([0-9]+)\. ]]; then
|
|
66
|
-
task_num="${BASH_REMATCH[1]}"
|
|
67
|
-
continue
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# 跳过非 Covers 行
|
|
71
|
-
if [[ "$line" != *"**Covers**:"* ]]; then
|
|
72
|
-
continue
|
|
73
|
-
fi
|
|
74
|
-
|
|
75
|
-
# 提取 Covers 内容
|
|
76
|
-
covers_content=$(echo "$line" \
|
|
77
|
-
| sed 's/.*\*\*Covers\*\*:[[:space:]]*//' \
|
|
78
|
-
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
79
|
-
|
|
80
|
-
# 格式检查:必须有 "Requirement:" 前缀
|
|
81
|
-
if [[ ! "$covers_content" =~ Requirement: ]]; then
|
|
82
|
-
semantic_warn "A1" "任务 $task_num 的 Covers 行格式不规范,跳过该任务的引用验证"
|
|
83
|
-
continue
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
# 提取 Requirement 名称(Requirement: 之后到第一个分隔符之前)
|
|
87
|
-
req_name=$(echo "$covers_content" \
|
|
88
|
-
| sed -n 's/.*Requirement:[[:space:]]*//p' \
|
|
89
|
-
| sed 's/[[:space:]]*[,,、].*//' \
|
|
90
|
-
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
91
|
-
|
|
92
|
-
if [ -z "$req_name" ]; then
|
|
93
|
-
semantic_warn "A1" "任务 $task_num 的 Covers 行缺少 Requirement 名称,跳过该任务的引用验证"
|
|
94
|
-
continue
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
# 校验 Requirement 存在性
|
|
98
|
-
if ! grep -qxF "$req_name" "$_req_file" 2>/dev/null; then
|
|
99
|
-
semantic_error "A1" "Covers 引用不存在: 任务 $task_num 引用了不存在的 Requirement \"$req_name\""
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
# 提取并校验 Scenario 名称列表
|
|
103
|
-
if [[ "$covers_content" =~ Scenario:[[:space:]]*(.+)$ ]]; then
|
|
104
|
-
scenarios_str="${BASH_REMATCH[1]}"
|
|
105
|
-
scenarios_str=$(echo "$scenarios_str" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
106
|
-
|
|
107
|
-
# 拆分 Scenario 名称(分隔符:、 , ,)
|
|
108
|
-
split_list=$(echo "$scenarios_str" \
|
|
109
|
-
| sed 's/[、,]/\'$'\n''/g' \
|
|
110
|
-
| sed 's/,[[:space:]]*/\'$'\n''/g')
|
|
111
|
-
|
|
112
|
-
while IFS= read -r scen_name; do
|
|
113
|
-
scen_name=$(echo "$scen_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
114
|
-
[ -z "$scen_name" ] && continue
|
|
115
|
-
|
|
116
|
-
if ! grep -qxF "${req_name}|${scen_name}" "$_scen_file" 2>/dev/null; then
|
|
117
|
-
semantic_error "A1" "Covers 引用不存在: 任务 $task_num 引用了不存在的 Scenario \"$scen_name\"(在 Requirement \"$req_name\" 下)"
|
|
118
|
-
fi
|
|
119
|
-
done <<< "$split_list"
|
|
120
|
-
fi
|
|
121
|
-
done < "$TASKS_FILE"
|
|
122
|
-
|
|
123
|
-
# ==== 清理临时文件 ====
|
|
124
|
-
rm -f "$_req_file" "$_scen_file"
|
|
125
|
-
}
|