specline 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -16
- package/package.json +1 -1
- package/templates/.cursor/agents/specline-spec-reviewer.md +11 -1
- package/templates/.cursor/commands/specline-quickfix.md +24 -0
- 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 +163 -14
- package/templates/.cursor/hooks/specline-reminder.sh +27 -19
- package/templates/.cursor/hooks/specline-session-start.sh +205 -80
- package/templates/.cursor/skills/specline-explore/SKILL.md +17 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +256 -22
- package/templates/.cursor/skills/specline-propose/SKILL.md +4 -1
- package/templates/.cursor/skills/specline-quickfix/SKILL.md +239 -0
package/README.md
CHANGED
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
/specline-pipeline "实现用户登录功能"
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
修 bug、改配置、文档微调?用轻量模式:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/specline-quickfix "修复登录按钮样式"
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
## 它能做什么
|
|
12
18
|
|
|
19
|
+
**完整流水线**(新功能、重构):
|
|
20
|
+
|
|
13
21
|
```
|
|
14
22
|
自然语言需求 → Spec → 审核 → 编码 → 审查 → 测试 → 归档
|
|
15
23
|
↑ ↑ ↑ ↑ ↑ ↑
|
|
@@ -18,12 +26,20 @@
|
|
|
18
26
|
并行 reviewer E2E
|
|
19
27
|
```
|
|
20
28
|
|
|
29
|
+
**轻量修复**(修 bug、改配置、文档微调):
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/ specline-quickfix "描述" → 理解代码 → 直接编辑 → Lint+自审 → 现有单测 → 轻量归档
|
|
33
|
+
0 个子 Agent 0 个人工确认 0 个 state 文件
|
|
34
|
+
```
|
|
35
|
+
|
|
21
36
|
每个阶段都经过 **确定性门禁校验** —— 用 `grep`、`jq`、编译器退出码、测试退出码判断通过与否。**质量判断零 LLM 参与**。
|
|
22
37
|
|
|
23
38
|
## 核心特性
|
|
24
39
|
|
|
25
40
|
- **需求驱动**:自然语言 → 结构化规格文档(Requirements + Scenarios + WHEN/THEN)
|
|
26
41
|
- **并行编码**:自动按前端/后端/config 拆分任务,同批次并发派发 Coding Agent
|
|
42
|
+
- **TDD 白盒测试**:无依赖任务自动启用 TDD 模式(先写单测 → 确认失败 → 最小实现 → 重构),与黑盒 test-writer 并行协作
|
|
27
43
|
- **确定性门禁**:每个阶段用 Shell 脚本的退出码判定是否通过,不做模糊判断
|
|
28
44
|
- **黑盒测试**:测试 Agent 只看 Spec 文档,不能读取任何实现源码
|
|
29
45
|
- **断点续跑**:随时中断,下次从最后一个可信门禁自动恢复(tasks.md 的 `[x]`/`[ ]` 标记进度)
|
|
@@ -59,8 +75,8 @@ specline sync --dry-run # 预览变更
|
|
|
59
75
|
my-project/
|
|
60
76
|
├── .cursor/
|
|
61
77
|
│ ├── agents/ ← 9 个 Specline Agent 定义
|
|
62
|
-
│ ├── commands/ ←
|
|
63
|
-
│ ├── skills/ ←
|
|
78
|
+
│ ├── commands/ ← 3 个 Slash 命令入口
|
|
79
|
+
│ ├── skills/ ← 6 个 Skill 指令
|
|
64
80
|
│ ├── hooks/ ← 7 个 Gate/Hook 脚本
|
|
65
81
|
│ └── hooks.json ← Cursor Hook 配置
|
|
66
82
|
├── specline/ ← 运行时目录
|
|
@@ -77,13 +93,36 @@ my-project/
|
|
|
77
93
|
/specline-pipeline "添加 JWT 用户认证"
|
|
78
94
|
```
|
|
79
95
|
|
|
96
|
+
小改动用快速模式:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/specline-quickfix "修改按钮颜色"
|
|
100
|
+
```
|
|
101
|
+
|
|
80
102
|
开始编码前先探索思路:
|
|
81
103
|
|
|
82
104
|
```
|
|
83
105
|
/specline-explore
|
|
84
106
|
```
|
|
85
107
|
|
|
86
|
-
##
|
|
108
|
+
## 工作流选择
|
|
109
|
+
|
|
110
|
+
Specline 提供两种工作流,按变更规模选择:
|
|
111
|
+
|
|
112
|
+
| 维度 | Quickfix (`/specline-quickfix`) | Pipeline (`/specline-pipeline`) |
|
|
113
|
+
|------|-------------------------------|-------------------------------|
|
|
114
|
+
| 文件改动数 | 1-3 个 | 4+ 个 |
|
|
115
|
+
| 关注点 | 单一关注点 | 多关注点/跨模块 |
|
|
116
|
+
| 架构变更 | 无新架构/新组件 | 需要新组件/新 API |
|
|
117
|
+
| 测试 | 不需要新测试 | 需要写新测试 |
|
|
118
|
+
| 典型场景 | 修 bug、改配置、文档微调 | 新增功能、重构 |
|
|
119
|
+
| 产出 | summary.md + files-changed.json | proposal/design/tasks/specs + 全部测试 |
|
|
120
|
+
| 人工确认 | 0 个 | 3 个 |
|
|
121
|
+
| 耗时 | 1-3 分钟 | 10-30 分钟 |
|
|
122
|
+
|
|
123
|
+
**使用建议**:如果不确定,优先用 quickfix。如果需要更严格的流程保证,用 pipeline。
|
|
124
|
+
|
|
125
|
+
## 完整流水线阶段
|
|
87
126
|
|
|
88
127
|
```
|
|
89
128
|
PHASE 1: SPEC(规格)
|
|
@@ -91,15 +130,16 @@ PHASE 1: SPEC(规格)
|
|
|
91
130
|
├── proposal.md — 需求提案(What/Why/Scope)
|
|
92
131
|
├── specs/*/spec.md — 功能规格(Requirements/Scenarios/WHEN-THEN)
|
|
93
132
|
├── design.md — 技术设计(架构/数据流/决策)
|
|
94
|
-
└── tasks.md — 任务清单(Type/Depends/Covers/Files + [ ] 进度标记)
|
|
133
|
+
└── tasks.md — 任务清单(Type/Depends/Covers/Testable/Files + [ ] 进度标记)
|
|
95
134
|
→ specline-spec-reviewer 审核
|
|
96
135
|
→ Gate: grep + jq 格式校验
|
|
97
136
|
→ 🟡 人工确认 Spec 和任务规划
|
|
98
137
|
|
|
99
138
|
PHASE 2: CODING(编码)
|
|
100
139
|
解析 tasks.md → 按依赖 DAG 分层 → 同批次前后端/config Agent 并发
|
|
140
|
+
无依赖 + 可测试任务 → 自动启用 TDD 模式(RED-GREEN-REFACTOR)
|
|
101
141
|
每完成一个任务,[ ] 自动标记为 [x]
|
|
102
|
-
→ Gate: 编译检查(tsc --noEmit / python -m compileall)
|
|
142
|
+
→ Gate: 编译检查(tsc --noEmit / python -m compileall) + 单元测试文件存在性检查
|
|
103
143
|
|
|
104
144
|
PHASE 3: REVIEW(审查)
|
|
105
145
|
specline-code-reviewer + specline-config-reviewer 分别审查代码和配置/文档
|
|
@@ -116,16 +156,55 @@ PHASE 5: ARCHIVE(归档)
|
|
|
116
156
|
→ delta specs 合并到主规格目录
|
|
117
157
|
→ 按日期归档到 specline/changes/archive/
|
|
118
158
|
✅ 完成
|
|
159
|
+
|
|
160
|
+
### TDD 白盒测试
|
|
161
|
+
|
|
162
|
+
Pipeline 采用「两层测试分离」架构:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Coding Agent(白盒 TDD) Test-Writer(黑盒)
|
|
166
|
+
───────────────────────── ─────────────────
|
|
167
|
+
产出: tests/unit/** 产出: tests/integration/**
|
|
168
|
+
tests/models/** tests/e2e/**
|
|
169
|
+
测试: 单个函数的输入输出 测试: 跨模块的用户行为
|
|
170
|
+
边界条件、异常路径 API 端到端契约
|
|
171
|
+
Spec Scenario 全覆盖
|
|
172
|
+
触发: 编码时同步产出 触发: Phase 2 与 Coding 并行启动
|
|
173
|
+
先写测试 → 确认失败 → 写实现 只读 Spec,不读源码
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
tasks.md 中 `Testable: true` 的任务自动启用 TDD 模式(完整 RED-GREEN-REFACTOR 循环),`Testable: false` 的任务保持原有流程。两个测试域严格目录隔离,冲突检测自动识别越界。
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 轻量修复流程
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
PHASE 1: UNDERSTAND(理解)
|
|
183
|
+
读取相关代码 → 理解上下文 → 意图模糊时 AskUserQuestion 确认
|
|
184
|
+
|
|
185
|
+
PHASE 2: IMPLEMENT(实现)
|
|
186
|
+
直接 Write/StrReplace 编辑 1-3 个文件
|
|
187
|
+
不需要 Spec 文档、DAG 构建、批次调度
|
|
188
|
+
|
|
189
|
+
PHASE 3: REVIEW(审查)
|
|
190
|
+
ReadLints 检查 + 自动修复(最多 2 次)→ Agent 自审逻辑正确性
|
|
191
|
+
|
|
192
|
+
PHASE 4: TEST(测试)
|
|
193
|
+
仅运行项目已有单元测试,无测试则跳过
|
|
194
|
+
失败自动修复最多 2 次
|
|
195
|
+
|
|
196
|
+
PHASE 5: ARCHIVE(归档)
|
|
197
|
+
生成 summary.md + files-changed.json → 询问是否 git commit
|
|
119
198
|
```
|
|
120
199
|
|
|
121
200
|
## 架构
|
|
122
201
|
|
|
123
202
|
```
|
|
124
|
-
/specline-pipeline ←
|
|
125
|
-
│
|
|
126
|
-
▼
|
|
127
|
-
specline-pipeline SKILL ←
|
|
128
|
-
│
|
|
203
|
+
/specline-pipeline ← 完整流水线(大功能) /specline-quickfix ← 轻量修复(小改动)
|
|
204
|
+
│ │
|
|
205
|
+
▼ ▼
|
|
206
|
+
specline-pipeline SKILL ← 编排层 编排者直接执行(无子 Agent)
|
|
207
|
+
│ Read → Write → ReadLints → Shell → 归档
|
|
129
208
|
┌───┼──────────────────┬──────────────────────┐
|
|
130
209
|
▼ ▼ ▼ ▼
|
|
131
210
|
9 个子 Agent specline-pipeline- Cursor Hooks
|
|
@@ -149,13 +228,13 @@ specline-pipeline SKILL ← 编排层(读状态、派发 Agent、调 Gate)
|
|
|
149
228
|
|-------|------|
|
|
150
229
|
| `specline-spec-creator` | 根据自然语言需求,基于内联模板直接生成 proposal/design/tasks/spec 四个文件 |
|
|
151
230
|
| `specline-spec-reviewer` | 审核规格的完整性、一致性和覆盖度 |
|
|
152
|
-
| `specline-frontend-dev` | UI
|
|
153
|
-
| `specline-backend-dev` | API
|
|
231
|
+
| `specline-frontend-dev` | UI 组件、页面、样式、交互逻辑(单个任务级别,Testable 任务启用 TDD) |
|
|
232
|
+
| `specline-backend-dev` | API 端点、数据模型、业务逻辑(单个任务级别,Testable 任务启用 TDD) |
|
|
154
233
|
| `specline-config-dev` | Shell 脚本、配置文件(JSON/YAML)、Markdown 文档(处理 Type: config/docs 任务) |
|
|
155
234
|
| `specline-code-reviewer` | 前端/后端代码质量、安全性、可维护性审查 |
|
|
156
235
|
| `specline-config-reviewer` | Shell 脚本安全性、配置文件语法和一致性、Markdown 文档结构审查 |
|
|
157
|
-
| `specline-test-writer` | 黑盒测试编写——只能看 Spec
|
|
158
|
-
| `specline-test-runner` | 执行测试并分类失败原因(测试问题/代码问题/Spec
|
|
236
|
+
| `specline-test-writer` | 黑盒测试编写——只能看 Spec 不能读源码,仅写集成测试(tests/integration/)和 E2E 测试 |
|
|
237
|
+
| `specline-test-runner` | 执行测试并分类失败原因(测试问题/代码问题/Spec 模糊),区分单元测试(回 coding agent)和集成/E2E 测试(回 test-writer) |
|
|
159
238
|
|
|
160
239
|
## 确定性门禁
|
|
161
240
|
|
|
@@ -163,8 +242,8 @@ specline-pipeline SKILL ← 编排层(读状态、派发 Agent、调 Gate)
|
|
|
163
242
|
|
|
164
243
|
| 门禁 | 检查内容 |
|
|
165
244
|
|------|---------|
|
|
166
|
-
| Spec | `grep` 检查 Purpose/Requirements/Scenarios 章节完整性、WHEN/THEN
|
|
167
|
-
| Build | `tsc --noEmit` / `python -m compileall` 编译检查 |
|
|
245
|
+
| Spec | `grep` 检查 Purpose/Requirements/Scenarios 章节完整性、WHEN/THEN 配对、Testable 字段格式与一致性 |
|
|
246
|
+
| Build | `tsc --noEmit` / `python -m compileall` 编译检查 + Testable 任务单元测试文件存在性与语法检查 |
|
|
168
247
|
| Lint | `ruff` / `eslint` 退出码 + code-review.json 中 error 数量 |
|
|
169
248
|
| Test | 测试框架退出码 + 覆盖率阈值 |
|
|
170
249
|
| Archive | 归档目录结构 + 必要文件完整性 |
|
package/package.json
CHANGED
|
@@ -49,6 +49,7 @@ description: 审核 spec.md、design.md、tasks.md 的完整性和一致性。
|
|
|
49
49
|
- 每个任务含 `Depends:` 标注
|
|
50
50
|
- 每个任务含 `Covers:` 标注(链接到具体的 Requirement 和 Scenario)
|
|
51
51
|
- 每个任务含 `Files:` 标注(非空,列出预期文件)
|
|
52
|
+
- 每个任务含 `Testable:` 标注(值在 true/false 范围内,可选但建议标注)
|
|
52
53
|
|
|
53
54
|
2. **独立性**:
|
|
54
55
|
- `Depends: (none)` 的任务占比 ≥ 50%(否则标记为 warning)
|
|
@@ -63,6 +64,12 @@ description: 审核 spec.md、design.md、tasks.md 的完整性和一致性。
|
|
|
63
64
|
- backend 类型的任务应涉及 API/模型/逻辑
|
|
64
65
|
- 没有 fullstack 类型(前端和后端必须拆开)
|
|
65
66
|
|
|
67
|
+
5. **Testable 合理性**:
|
|
68
|
+
- `Testable: true` 的任务必须满足:Depends: (none) + Type ≠ config/docs + 有可拆分的独立逻辑单元
|
|
69
|
+
- `Testable: false` 的任务如果同时满足 Depends: (none) + Type ≠ config/docs + 有独立逻辑单元 → warning(建议标记为 Testable: true)
|
|
70
|
+
- 有上游依赖的任务 Testable 必须为 false
|
|
71
|
+
- Type 为 config/docs 的任务 Testable 必须为 false
|
|
72
|
+
|
|
66
73
|
## 输出格式
|
|
67
74
|
|
|
68
75
|
产出 `specline/changes/<change-name>/specs/<capability>/spec-review.json`:
|
|
@@ -81,6 +88,7 @@ description: 审核 spec.md、design.md、tasks.md 的完整性和一致性。
|
|
|
81
88
|
"total": 6,
|
|
82
89
|
"independent": 4,
|
|
83
90
|
"parallel_ratio": 0.67,
|
|
91
|
+
"testable_count": 3,
|
|
84
92
|
"types": { "frontend": 2, "backend": 3, "config": 1 }
|
|
85
93
|
},
|
|
86
94
|
"design_review": {
|
|
@@ -98,11 +106,13 @@ description: 审核 spec.md、design.md、tasks.md 的完整性和一致性。
|
|
|
98
106
|
"[spec.md] 缺少异常路径场景:未定义 'worker 数量为 0' 时的行为",
|
|
99
107
|
"[tasks.md] 任务 3 缺少 Covers 标注",
|
|
100
108
|
"[tasks.md] 任务 1 和 任务 2 的 Files 有交集:都包含了 src/utils/api.ts",
|
|
109
|
+
"[tasks.md] 任务 3 Testable=true 但存在上游依赖 (Depends: 1),应为 false",
|
|
110
|
+
"[tasks.md] 任务 5 (Depends: none, Type: backend) 建议标记为 Testable: true",
|
|
101
111
|
"[design.md] 提到使用 Redis 缓存,但 tasks.md 中没有对应的 infra 任务",
|
|
102
112
|
"[coverage] Scenario '用户登出' 未被任何任务覆盖"
|
|
103
113
|
],
|
|
104
114
|
"coverage": { "requirements_covered": 4, "requirements_total": 5, "scenarios_covered": 10, "scenarios_total": 14 },
|
|
105
|
-
"task_stats": { "total": 6, "independent": 4, "parallel_ratio": 0.67, "types": { "frontend": 2, "backend": 3, "config": 1 } },
|
|
115
|
+
"task_stats": { "total": 6, "independent": 4, "parallel_ratio": 0.67, "testable_count": 3, "types": { "frontend": 2, "backend": 3, "config": 1 } },
|
|
106
116
|
"design_review": { "issues": ["Redis 缓存方案缺少对应的 infra 任务"] }
|
|
107
117
|
}
|
|
108
118
|
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: /specline-quickfix
|
|
3
|
+
id: specline-quickfix
|
|
4
|
+
category: Workflow
|
|
5
|
+
description: 轻量修改 Skill —— 小改动用 quickfix,大功能用 pipeline
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
轻量修改流程,在不创建规划文档的情况下快速执行小改动。
|
|
9
|
+
|
|
10
|
+
**用法:**
|
|
11
|
+
- `/specline-quickfix <描述>` — 执行轻量修改流程(修 bug、改配置、文档微调等小改动)
|
|
12
|
+
|
|
13
|
+
**阶段:**
|
|
14
|
+
1. UNDERSTAND — 读取相关代码,理解变更上下文
|
|
15
|
+
2. IMPLEMENT — 直接编辑文件(单 Agent,无子 Agent)
|
|
16
|
+
3. REVIEW — ReadLints 自动校验 + 修复(最多 2 次)+ Agent 自审
|
|
17
|
+
4. TEST — 运行项目已有单元测试(失败修复最多 2 次),无测试则跳过
|
|
18
|
+
5. ARCHIVE — 生成 summary.md + files-changed.json,询问 git commit
|
|
19
|
+
|
|
20
|
+
**适用场景**:1-3 个文件改动、单一关注点、不涉及架构变更、不需要新测试。
|
|
21
|
+
|
|
22
|
+
**不适合的场景**(应使用 `/specline-pipeline`):4+ 个文件改动、新增功能/重构、需要新测试、跨模块/多关注点。
|
|
23
|
+
|
|
24
|
+
0 个人工确认点,全程自动质量保证。
|
|
@@ -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
|
|
|
@@ -15,19 +15,27 @@
|
|
|
15
15
|
|
|
16
16
|
set -euo pipefail
|
|
17
17
|
|
|
18
|
-
# ===== 参数解析 =====
|
|
19
18
|
PHASE="${1:-}"
|
|
20
19
|
CHANGE=""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
EXECUTE_ARCHIVE=""
|
|
21
|
+
|
|
22
|
+
# 遍历所有参数,不依赖位置
|
|
23
|
+
shift # 跳过 PHASE
|
|
24
|
+
while [ $# -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--change)
|
|
27
|
+
CHANGE="$2"
|
|
28
|
+
shift 2
|
|
29
|
+
;;
|
|
30
|
+
--execute)
|
|
31
|
+
EXECUTE_ARCHIVE="--execute"
|
|
32
|
+
shift
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
shift
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
31
39
|
# ===== 项目根目录 =====
|
|
32
40
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
41
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
@@ -300,7 +308,31 @@ gate_spec() {
|
|
|
300
308
|
fi
|
|
301
309
|
pass "tasks.md 标注完整性检查通过 ($task_count 个任务)"
|
|
302
310
|
|
|
303
|
-
# 10.
|
|
311
|
+
# 10. Testable 字段校验
|
|
312
|
+
local testable_count
|
|
313
|
+
testable_count=$(grep -c '\*\*Testable\*\*:' "$tasks_file" || echo "0")
|
|
314
|
+
|
|
315
|
+
if [ "$testable_count" -eq 0 ]; then
|
|
316
|
+
echo "⚠️ Testable 标注缺失(向后兼容模式:缺失字段的任务将被视为 Testable: false)"
|
|
317
|
+
elif [ "$testable_count" -gt 0 ] && [ "$testable_count" -lt "$task_count" ]; then
|
|
318
|
+
local missing_testable_tasks
|
|
319
|
+
missing_testable_tasks=$(awk '
|
|
320
|
+
/^## / {
|
|
321
|
+
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
322
|
+
prev_task = $2; gsub(/\..*/, "", prev_task)
|
|
323
|
+
in_task = 1; has_testable = 0
|
|
324
|
+
}
|
|
325
|
+
/\*\*Testable\*\*:/ { has_testable = 1 }
|
|
326
|
+
END {
|
|
327
|
+
if (in_task && !has_testable) missing = missing (missing ? ", " : "") prev_task
|
|
328
|
+
print missing
|
|
329
|
+
}' "$tasks_file")
|
|
330
|
+
echo "⚠️ Testable 标注不完整:任务=$task_count, Testable=$testable_count(缺失任务: $missing_testable_tasks;缺失字段的任务将被视为 Testable: false)"
|
|
331
|
+
else
|
|
332
|
+
pass "Testable 标注完整性检查通过 ($testable_count/$task_count)"
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# 11. 至少 1 个任务无依赖
|
|
304
336
|
local independent_count
|
|
305
337
|
independent_count=$(grep -c '\*\*Depends\*\*: (none)' "$tasks_file" || echo "0")
|
|
306
338
|
if [ "$independent_count" -lt 1 ]; then
|
|
@@ -337,6 +369,86 @@ gate_build() {
|
|
|
337
369
|
pass "Python 语法检查通过"
|
|
338
370
|
fi
|
|
339
371
|
|
|
372
|
+
# 单元测试文件存在性检查(Testable=true 任务)
|
|
373
|
+
local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
|
|
374
|
+
if [ -f "$tasks_file" ]; then
|
|
375
|
+
local testable_true_count
|
|
376
|
+
testable_true_count=$(grep -c '\*\*Testable\*\*:.*true' "$tasks_file" || echo "0")
|
|
377
|
+
|
|
378
|
+
if [ "$testable_true_count" -gt 0 ]; then
|
|
379
|
+
echo "正在检查 $testable_true_count 个 Testable=true 任务的单元测试文件..."
|
|
380
|
+
|
|
381
|
+
local missing_files=""
|
|
382
|
+
local syntax_errors=""
|
|
383
|
+
|
|
384
|
+
while IFS='|' read -r task_id file_path; do
|
|
385
|
+
if [ -z "$file_path" ]; then
|
|
386
|
+
missing_files="${missing_files}
|
|
387
|
+
任务 $task_id: 未在 Files 列表中声明 tests/unit/ 或 tests/models/ 下的测试文件"
|
|
388
|
+
continue
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
if [ ! -f "$PROJECT_ROOT/$file_path" ]; then
|
|
392
|
+
missing_files="${missing_files}
|
|
393
|
+
任务 $task_id: $file_path"
|
|
394
|
+
continue
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
# 语法检查
|
|
398
|
+
case "$file_path" in
|
|
399
|
+
*.py)
|
|
400
|
+
if ! python -m py_compile "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
401
|
+
syntax_errors="${syntax_errors}
|
|
402
|
+
任务 $task_id: $file_path (Python 语法错误)"
|
|
403
|
+
fi
|
|
404
|
+
;;
|
|
405
|
+
*.ts|*.tsx)
|
|
406
|
+
if ! npx tsc --noEmit "$PROJECT_ROOT/$file_path" 2>&1; then
|
|
407
|
+
syntax_errors="${syntax_errors}
|
|
408
|
+
任务 $task_id: $file_path (TypeScript 语法错误)"
|
|
409
|
+
fi
|
|
410
|
+
;;
|
|
411
|
+
esac
|
|
412
|
+
done < <(awk '
|
|
413
|
+
/^## / {
|
|
414
|
+
task_id = $2; gsub(/\..*/, "", task_id)
|
|
415
|
+
testable = ""; files_line = ""
|
|
416
|
+
}
|
|
417
|
+
/\*\*Testable\*\*:.*true/ { testable = "true" }
|
|
418
|
+
/\*\*Files\*\*:/ {
|
|
419
|
+
if (testable == "true") {
|
|
420
|
+
files_line = $0
|
|
421
|
+
gsub(/.*\*\*Files\*\*:[ \t]*/, "", files_line)
|
|
422
|
+
split(files_line, paths, /,[ \t]*/)
|
|
423
|
+
has_unit_test = 0
|
|
424
|
+
for (i in paths) {
|
|
425
|
+
gsub(/^[ \t]+|[ \t]+$/, "", paths[i])
|
|
426
|
+
if (paths[i] ~ /^tests\/(unit|models)\//) {
|
|
427
|
+
print task_id "|" paths[i]
|
|
428
|
+
has_unit_test = 1
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (has_unit_test == 0) {
|
|
432
|
+
print task_id "|"
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
' "$tasks_file")
|
|
437
|
+
|
|
438
|
+
if [ -n "$missing_files" ]; then
|
|
439
|
+
fail "单元测试文件缺失:${missing_files}"
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
if [ -n "$syntax_errors" ]; then
|
|
443
|
+
fail "单元测试文件语法错误:${syntax_errors}"
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
pass "单元测试文件存在性检查通过"
|
|
447
|
+
else
|
|
448
|
+
echo "ℹ️ 无 Testable=true 任务,跳过单元测试文件检查"
|
|
449
|
+
fi
|
|
450
|
+
fi
|
|
451
|
+
|
|
340
452
|
write_gate_passed "phases.coding.gates.build_gate"
|
|
341
453
|
pass "Build Gate 全部通过"
|
|
342
454
|
}
|
|
@@ -551,7 +663,7 @@ gate_archive() {
|
|
|
551
663
|
fi
|
|
552
664
|
|
|
553
665
|
# 如果传了 --execute,执行实际归档动作
|
|
554
|
-
if [ "$
|
|
666
|
+
if [ -n "$EXECUTE_ARCHIVE" ]; then
|
|
555
667
|
local src_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
|
|
556
668
|
local archive_dir="$PROJECT_ROOT/specline/changes/archive"
|
|
557
669
|
local date_prefix
|
|
@@ -595,6 +707,15 @@ gate_archive() {
|
|
|
595
707
|
sed -i '' "s/\"current_phase\": \"[^\"]*\"/\"current_phase\": \"archived\"/g" "$dest/.pipeline-state.json" 2>/dev/null || true
|
|
596
708
|
fi
|
|
597
709
|
|
|
710
|
+
# 清理所有绑定到该 change 的 session
|
|
711
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
712
|
+
if [ -f "$bindings_file" ]; then
|
|
713
|
+
jq --arg change "$CHANGE" \
|
|
714
|
+
'with_entries(select(.value.change != $change))' \
|
|
715
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
716
|
+
echo "✅ 已清理 pipeline '$CHANGE' 的所有 session 绑定"
|
|
717
|
+
fi
|
|
718
|
+
|
|
598
719
|
exit 0
|
|
599
720
|
fi
|
|
600
721
|
|
|
@@ -639,6 +760,31 @@ gate_status() {
|
|
|
639
760
|
}' "$STATE_FILE"
|
|
640
761
|
}
|
|
641
762
|
|
|
763
|
+
gate_bind() {
|
|
764
|
+
local session_id="$1"
|
|
765
|
+
local target_change="$2"
|
|
766
|
+
|
|
767
|
+
if [ -z "$session_id" ] || [ -z "$target_change" ]; then
|
|
768
|
+
fail "需要 <session_id> <change_name>"
|
|
769
|
+
fi
|
|
770
|
+
|
|
771
|
+
local state_file="$PROJECT_ROOT/specline/changes/$target_change/.pipeline-state.json"
|
|
772
|
+
if [ ! -f "$state_file" ]; then
|
|
773
|
+
fail "Change '$target_change' 不存在"
|
|
774
|
+
fi
|
|
775
|
+
|
|
776
|
+
local bindings_file="$PROJECT_ROOT/specline/.pipeline-sessions.json"
|
|
777
|
+
[ ! -f "$bindings_file" ] && echo '{}' > "$bindings_file"
|
|
778
|
+
|
|
779
|
+
local now
|
|
780
|
+
now=$(now_iso8601)
|
|
781
|
+
jq --arg sid "$session_id" --arg change "$target_change" --arg time "$now" \
|
|
782
|
+
'.[$sid] = {"change": $change, "bound_at": $time}' \
|
|
783
|
+
"$bindings_file" > "${bindings_file}.tmp" && mv "${bindings_file}.tmp" "$bindings_file"
|
|
784
|
+
|
|
785
|
+
echo "✅ 已绑定 session '$session_id' → pipeline '$target_change'"
|
|
786
|
+
}
|
|
787
|
+
|
|
642
788
|
# ===== 分派 =====
|
|
643
789
|
|
|
644
790
|
case "$PHASE" in
|
|
@@ -669,9 +815,12 @@ case "$PHASE" in
|
|
|
669
815
|
test-e2e)
|
|
670
816
|
gate_test_e2e
|
|
671
817
|
;;
|
|
818
|
+
bind)
|
|
819
|
+
gate_bind "$2" "$3"
|
|
820
|
+
;;
|
|
672
821
|
archive)
|
|
673
822
|
gate_archive "$@"
|
|
674
|
-
;;
|
|
823
|
+
;;
|
|
675
824
|
status)
|
|
676
825
|
gate_status
|
|
677
826
|
;;
|