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.
Files changed (43) hide show
  1. package/core/agents/specline-spec-creator.yaml +16 -0
  2. package/core/agents/specline-spec-reviewer.yaml +14 -2
  3. package/core/skills/specline-pipeline/SKILL.md +63 -9
  4. package/lib/merge.mjs +7 -3
  5. package/package.json +1 -1
  6. package/templates/.cursor/README.md +0 -18
  7. package/templates/.cursor/agents/specline-backend-dev.md +0 -47
  8. package/templates/.cursor/agents/specline-code-reviewer.md +0 -110
  9. package/templates/.cursor/agents/specline-config-dev.md +0 -52
  10. package/templates/.cursor/agents/specline-config-reviewer.md +0 -79
  11. package/templates/.cursor/agents/specline-explore-assistant.md +0 -81
  12. package/templates/.cursor/agents/specline-frontend-dev.md +0 -47
  13. package/templates/.cursor/agents/specline-spec-creator.md +0 -376
  14. package/templates/.cursor/agents/specline-spec-reviewer.md +0 -144
  15. package/templates/.cursor/agents/specline-test-runner.md +0 -107
  16. package/templates/.cursor/agents/specline-test-writer.md +0 -170
  17. package/templates/.cursor/hooks/specline-agent-guard.sh +0 -131
  18. package/templates/.cursor/hooks/specline-auto-format.sh +0 -12
  19. package/templates/.cursor/hooks/specline-phase-guard.sh +0 -201
  20. package/templates/.cursor/hooks/specline-pipeline-gate-checks/a1-covers-ref.sh +0 -125
  21. package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +0 -171
  22. package/templates/.cursor/hooks/specline-pipeline-gate-checks/c1-exception.sh +0 -71
  23. package/templates/.cursor/hooks/specline-pipeline-gate-checks/c2-vague.sh +0 -60
  24. package/templates/.cursor/hooks/specline-pipeline-gate-checks/common.sh +0 -68
  25. package/templates/.cursor/hooks/specline-pipeline-gate-checks/d1-cycle.sh +0 -149
  26. package/templates/.cursor/hooks/specline-pipeline-gate-checks/d3-type-file.sh +0 -260
  27. package/templates/.cursor/hooks/specline-pipeline-gate.sh +0 -1569
  28. package/templates/.cursor/hooks/specline-reminder.sh +0 -147
  29. package/templates/.cursor/hooks/specline-session-start.sh +0 -259
  30. package/templates/.cursor/hooks/specline-shell-guard.sh +0 -18
  31. package/templates/.cursor/hooks.json +0 -46
  32. package/templates/.cursor/skills/specline-apply-change/SKILL.md +0 -197
  33. package/templates/.cursor/skills/specline-archive-change/SKILL.md +0 -173
  34. package/templates/.cursor/skills/specline-explore/SKILL.md +0 -504
  35. package/templates/.cursor/skills/specline-knowledge/SKILL.md +0 -539
  36. package/templates/.cursor/skills/specline-pipeline/SKILL.md +0 -616
  37. package/templates/.cursor/skills/specline-pipeline/references/error-recovery-details.md +0 -49
  38. package/templates/.cursor/skills/specline-pipeline/references/event-log-spec.md +0 -59
  39. package/templates/.cursor/skills/specline-pipeline/references/pipeline-state-schema.md +0 -87
  40. package/templates/.cursor/skills/specline-pipeline/templates/subagent-prompts.md +0 -253
  41. package/templates/.cursor/skills/specline-propose/SKILL.md +0 -186
  42. package/templates/.cursor/skills/specline-quickfix/SKILL.md +0 -265
  43. 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
- }