specline 1.0.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,682 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # specline-pipeline-gate.sh — 确定性门禁脚本(零 LLM 参与)
4
+ #
5
+ # Usage:
6
+ # specline-pipeline-gate.sh <phase> --change <change-name>
7
+ #
8
+ # Phases:
9
+ # new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status
10
+ #
11
+ # Exit codes:
12
+ # 0 = 通过
13
+ # 1 = 失败
14
+ # 2 = 输入参数错误
15
+
16
+ set -euo pipefail
17
+
18
+ # ===== 参数解析 =====
19
+ PHASE="${1:-}"
20
+ CHANGE=""
21
+ if [ "$#" -ge 3 ] && [ "$2" = "--change" ]; then
22
+ CHANGE="$3"
23
+ fi
24
+
25
+ if [ -z "$PHASE" ]; then
26
+ echo "Usage: specline-pipeline-gate.sh <phase> --change <change-name>"
27
+ echo "Phases: new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status"
28
+ exit 2
29
+ fi
30
+
31
+ # ===== 项目根目录 =====
32
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
33
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
34
+
35
+ # ===== 状态文件 =====
36
+ if [ -n "$CHANGE" ]; then
37
+ STATE_FILE="$PROJECT_ROOT/specline/changes/$CHANGE/.pipeline-state.json"
38
+ else
39
+ STATE_FILE=""
40
+ fi
41
+
42
+ # ===== 辅助函数 =====
43
+ now_iso8601() {
44
+ date -u +"%Y-%m-%dT%H:%M:%SZ"
45
+ }
46
+
47
+ write_gate_passed() {
48
+ local gate_path="$1" # e.g., "phases.spec.gates.spec_gate"
49
+ if [ -n "$STATE_FILE" ] && [ -f "$STATE_FILE" ]; then
50
+ local time
51
+ time=$(now_iso8601)
52
+ jq --arg time "$time" \
53
+ ".updated_at = \$time | .${gate_path} = { \"passed\": true, \"run_at\": \$time }" \
54
+ "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
55
+ fi
56
+ }
57
+
58
+ fail() {
59
+ echo "❌ $1" >&2
60
+ exit 1
61
+ }
62
+
63
+ pass() {
64
+ echo "✅ $1"
65
+ }
66
+
67
+ # ===== 获取 Spec 文件路径 =====
68
+ find_spec_file() {
69
+ if [ -z "$CHANGE" ]; then
70
+ echo ""
71
+ return
72
+ fi
73
+ find "$PROJECT_ROOT/specline/changes/$CHANGE/specs" -name "spec.md" 2>/dev/null | head -1
74
+ }
75
+
76
+ # ===== Phase Handlers =====
77
+
78
+ gate_new() {
79
+ if [ -z "$CHANGE" ]; then
80
+ fail "需要 --change <name>"
81
+ fi
82
+
83
+ local change_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
84
+
85
+ if [ -d "$change_dir" ]; then
86
+ echo "⚠️ Change '$CHANGE' 已存在"
87
+ exit 0
88
+ fi
89
+
90
+ mkdir -p "$change_dir/specs"
91
+
92
+ # 写入 .specline.yaml
93
+ cat > "$change_dir/.specline.yaml" << YAML
94
+ schema: spec-driven
95
+ created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
96
+ YAML
97
+
98
+ # 初始化 .pipeline-state.json
99
+ cat > "$change_dir/.pipeline-state.json" << 'JSON'
100
+ {
101
+ "version": 1,
102
+ "change_name": "CHANGE_NAME_PLACEHOLDER",
103
+ "created_at": "CREATED_AT_PLACEHOLDER",
104
+ "updated_at": "CREATED_AT_PLACEHOLDER",
105
+ "current_phase": "spec",
106
+ "current_step": "spec-creator",
107
+ "phases": {
108
+ "spec": { "status": "in_progress", "retry_count": 0, "sub_phases": {}, "gates": { "spec_gate": { "passed": null }, "human_gate_1": { "passed": null } } },
109
+ "coding": { "status": "pending", "tasks": [], "sub_phases": {}, "gates": { "build_gate": { "passed": null } } },
110
+ "code_review": { "status": "pending", "retry_count": 0, "gates": { "lint_gate": { "passed": null }, "human_gate_2": { "passed": null } } },
111
+ "test": { "status": "pending", "framework": null, "sub_phases": { "unit": { "status": "pending", "gates": { "test_unit_gate": { "passed": null } } }, "integration": { "status": "pending", "gates": { "test_integration_gate": { "passed": null } } }, "e2e": { "status": "pending", "gates": { "test_e2e_gate": { "passed": null } } } } },
112
+ "archive": { "status": "pending", "gates": { "human_gate_3": { "passed": null }, "archive_gate": { "passed": null } } }
113
+ }
114
+ }
115
+ JSON
116
+
117
+ # 用实际值替换占位符
118
+ local now
119
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
120
+ # macOS sed 兼容
121
+ sed -i '' "s/CHANGE_NAME_PLACEHOLDER/$CHANGE/g" "$change_dir/.pipeline-state.json"
122
+ sed -i '' "s/CREATED_AT_PLACEHOLDER/$now/g" "$change_dir/.pipeline-state.json"
123
+
124
+ echo "✅ Change '$CHANGE' 已创建: $change_dir"
125
+ echo " .specline.yaml + .pipeline-state.json + specs/"
126
+
127
+ write_gate_passed "phases.spec.gates.spec_gate"
128
+ }
129
+
130
+ gate_list() {
131
+ local changes_dir="$PROJECT_ROOT/specline/changes"
132
+ local json_output=false
133
+
134
+ if [ "${1:-}" = "--json" ]; then
135
+ json_output=true
136
+ fi
137
+
138
+ if [ ! -d "$changes_dir" ]; then
139
+ if $json_output; then
140
+ echo '[]'
141
+ else
142
+ echo "(无活跃 change)"
143
+ fi
144
+ exit 0
145
+ fi
146
+
147
+ if $json_output; then
148
+ echo "["
149
+ local first=true
150
+ for f in "$changes_dir"/*/.pipeline-state.json; do
151
+ [ -f "$f" ] || continue
152
+ # 跳过 archive/
153
+ if echo "$f" | grep -q "/archive/"; then continue; fi
154
+ local dir name phase
155
+ dir=$(dirname "$f")
156
+ name=$(basename "$dir")
157
+ phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
158
+ if [ "$first" = true ]; then first=false; else echo ","; fi
159
+ echo " {\"name\":\"$name\",\"phase\":\"$phase\"}"
160
+ done
161
+ echo "]"
162
+ else
163
+ for f in "$changes_dir"/*/.pipeline-state.json; do
164
+ [ -f "$f" ] || continue
165
+ if echo "$f" | grep -q "/archive/"; then continue; fi
166
+ local dir name phase
167
+ dir=$(dirname "$f")
168
+ name=$(basename "$dir")
169
+ phase=$(jq -r '.current_phase // "unknown"' "$f" 2>/dev/null)
170
+ echo " $name (phase: $phase)"
171
+ done
172
+ fi
173
+ }
174
+
175
+ gate_artifacts() {
176
+ if [ -z "$CHANGE" ]; then
177
+ fail "需要 --change <name>"
178
+ fi
179
+
180
+ local dir="$PROJECT_ROOT/specline/changes/$CHANGE"
181
+ local json_output=false
182
+
183
+ if [ "${1:-}" = "--json" ]; then
184
+ json_output=true
185
+ fi
186
+
187
+ local has_proposal=false has_design=false has_tasks=false has_specs=false
188
+
189
+ [ -f "$dir/proposal.md" ] && has_proposal=true
190
+ [ -f "$dir/design.md" ] && has_design=true
191
+ [ -f "$dir/tasks.md" ] && has_tasks=true
192
+ [ -d "$dir/specs" ] && [ -n "$(find "$dir/specs" -name 'spec.md' 2>/dev/null)" ] && has_specs=true
193
+
194
+ if $json_output; then
195
+ echo "{"
196
+ echo " \"proposal\": $has_proposal,"
197
+ echo " \"design\": $has_design,"
198
+ echo " \"tasks\": $has_tasks,"
199
+ echo " \"specs\": $has_specs"
200
+ echo "}"
201
+ else
202
+ echo "Artifacts for '$CHANGE':"
203
+ echo " proposal.md: $has_proposal"
204
+ echo " design.md: $has_design"
205
+ echo " tasks.md: $has_tasks"
206
+ echo " spec.md: $has_specs"
207
+ fi
208
+ }
209
+
210
+ gate_spec() {
211
+ local spec_file
212
+ spec_file=$(find_spec_file)
213
+
214
+ if [ -z "$spec_file" ] || [ ! -f "$spec_file" ]; then
215
+ fail "spec.md 不存在。请确保 spec-creator 已生成 spec 文件。"
216
+ fi
217
+
218
+ # 1. H1 含 "Specification"
219
+ if ! grep -q "^# .* Specification" "$spec_file"; then
220
+ fail "标题格式错误:H1 必须包含 'Specification' 关键词"
221
+ fi
222
+
223
+ # 2. 含 Purpose 章节
224
+ if ! grep -q "^## Purpose" "$spec_file"; then
225
+ fail "缺少 ## Purpose 章节"
226
+ fi
227
+
228
+ # 3. 含 Requirements 章节
229
+ if ! grep -q "^## Requirements" "$spec_file"; then
230
+ fail "缺少 ## Requirements 章节"
231
+ fi
232
+
233
+ # 4. 至少 1 个 Requirement
234
+ local req_count
235
+ req_count=$(grep -c "^### Requirement:" "$spec_file" || echo "0")
236
+ if [ "$req_count" -lt 1 ]; then
237
+ fail "至少需要 1 个 Requirement,当前: $req_count"
238
+ fi
239
+ pass "Requirements 数量: $req_count"
240
+
241
+ # 5. 每个 Requirement 至少 1 个 Scenario(简化检查:Scenario 总数 >= Requirement 总数)
242
+ local scenario_count
243
+ scenario_count=$(grep -c "^#### Scenario:" "$spec_file" || echo "0")
244
+ if [ "$scenario_count" -lt "$req_count" ]; then
245
+ fail "每个 Requirement 至少需要 1 个 Scenario。Requirement: $req_count, Scenario: $scenario_count"
246
+ fi
247
+ pass "Scenario 数量: $scenario_count"
248
+
249
+ # 6. WHEN/THEN 配对检查
250
+ local when_count then_count
251
+ when_count=$(grep -c "\*\*WHEN\*\*" "$spec_file" || echo "0")
252
+ then_count=$(grep -c "\*\*THEN\*\*" "$spec_file" || echo "0")
253
+ if [ "$when_count" -ne "$then_count" ]; then
254
+ fail "WHEN/THEN 数量不匹配。WHEN: $when_count, THEN: $then_count"
255
+ fi
256
+ pass "WHEN/THEN 配对检查通过 ($when_count 对)"
257
+
258
+ # 7. review.json 状态检查(如果存在)
259
+ local review_file
260
+ review_file="$(dirname "$spec_file")/spec-review.json"
261
+ if [ -f "$review_file" ]; then
262
+ local review_status
263
+ review_status=$(jq -r '.status' "$review_file" 2>/dev/null || echo "missing")
264
+ if [ "$review_status" != "approved" ]; then
265
+ fail "spec-review.json 审核未通过 (status: $review_status)"
266
+ fi
267
+ pass "审核状态: approved"
268
+
269
+ # 7b. 检查 coverage(所有 Requirement 和 Scenario 被 task 的 Covers 覆盖)
270
+ local cov_req_total cov_req_covered
271
+ cov_req_total=$(jq -r '.coverage.requirements_total' "$review_file" 2>/dev/null || echo "0")
272
+ cov_req_covered=$(jq -r '.coverage.requirements_covered' "$review_file" 2>/dev/null || echo "0")
273
+ if [ "$cov_req_covered" -lt "$cov_req_total" ]; then
274
+ fail "Requirement 覆盖不全: $cov_req_covered/$cov_req_total"
275
+ fi
276
+ pass "Requirement 覆盖率: $cov_req_covered/$cov_req_total"
277
+ else
278
+ pass "审核状态: 无 spec-review.json(跳过审核检查)"
279
+ fi
280
+
281
+ # 8. 检查 tasks.md 是否存在且含完整的 Type/Depends/Covers/Files 标注
282
+ local tasks_file="$PROJECT_ROOT/specline/changes/$CHANGE/tasks.md"
283
+ if [ ! -f "$tasks_file" ]; then
284
+ fail "tasks.md 不存在"
285
+ fi
286
+ pass "tasks.md 存在"
287
+
288
+ # 9. 检查每个任务标注完整性
289
+ local task_count type_count deps_count covers_count files_count
290
+ task_count=$(grep -c '^## ' "$tasks_file" || echo "0")
291
+ type_count=$(grep -c '\*\*Type\*\*:' "$tasks_file" || echo "0")
292
+ deps_count=$(grep -c '\*\*Depends\*\*:' "$tasks_file" || echo "0")
293
+ covers_count=$(grep -c '\*\*Covers\*\*:' "$tasks_file" || echo "0")
294
+ files_count=$(grep -c '\*\*Files\*\*:' "$tasks_file" || echo "0")
295
+
296
+ if [ "$type_count" -lt "$task_count" ] || [ "$deps_count" -lt "$task_count" ] || \
297
+ [ "$covers_count" -lt "$task_count" ] || [ "$files_count" -lt "$task_count" ]; then
298
+ fail "tasks.md 标注不完整:任务=$task_count, Type=$type_count, Depends=$deps_count, Covers=$covers_count, Files=$files_count"
299
+ fi
300
+ pass "tasks.md 标注完整性检查通过 ($task_count 个任务)"
301
+
302
+ # 10. 至少 1 个任务无依赖
303
+ local independent_count
304
+ independent_count=$(grep -c '\*\*Depends\*\*: (none)' "$tasks_file" || echo "0")
305
+ if [ "$independent_count" -lt 1 ]; then
306
+ fail "至少需要 1 个无依赖任务 (Depends: none),当前: $independent_count"
307
+ fi
308
+ pass "无依赖任务数: $independent_count"
309
+
310
+ write_gate_passed "phases.spec.gates.spec_gate"
311
+ pass "Spec Gate 全部通过"
312
+ }
313
+
314
+ gate_build() {
315
+ # TypeScript 编译检查(如果存在 tsconfig.json)
316
+ if [ -f "$PROJECT_ROOT/tsconfig.json" ]; then
317
+ echo "正在检查 TypeScript 编译..."
318
+ if ! npx tsc --noEmit 2>&1; then
319
+ fail "TypeScript 编译失败"
320
+ fi
321
+ pass "TypeScript 编译通过"
322
+ fi
323
+
324
+ # Python 语法检查
325
+ echo "正在检查 Python 语法..."
326
+ local py_dirs=""
327
+ for d in agent server scripts; do
328
+ if [ -d "$PROJECT_ROOT/$d" ]; then
329
+ py_dirs="$py_dirs $d"
330
+ fi
331
+ done
332
+ if [ -n "$py_dirs" ]; then
333
+ if ! python -m compileall -q $py_dirs 2>&1; then
334
+ fail "Python 语法错误"
335
+ fi
336
+ pass "Python 语法检查通过"
337
+ fi
338
+
339
+ write_gate_passed "phases.coding.gates.build_gate"
340
+ pass "Build Gate 全部通过"
341
+ }
342
+
343
+ gate_lint() {
344
+ # Python lint (ruff)
345
+ if command -v ruff &>/dev/null; then
346
+ echo "正在检查 Python 代码规范..."
347
+ if ! ruff check "$PROJECT_ROOT" --quiet 2>&1; then
348
+ fail "Python lint 失败"
349
+ fi
350
+ pass "Python lint 通过"
351
+ else
352
+ echo "⚠️ ruff 未安装,跳过 Python lint"
353
+ fi
354
+
355
+ # JS/TS lint (eslint)
356
+ if [ -f "$PROJECT_ROOT/package.json" ]; then
357
+ if command -v npx &>/dev/null; then
358
+ echo "正在检查 JS/TS 代码规范..."
359
+ if ! npx eslint "$PROJECT_ROOT" --max-warnings 0 --quiet 2>&1; then
360
+ fail "JS/TS lint 失败"
361
+ fi
362
+ pass "JS/TS lint 通过"
363
+ fi
364
+ fi
365
+
366
+ # code-review.json error 计数
367
+ local review_file="$PROJECT_ROOT/code-review.json"
368
+ if [ -f "$review_file" ]; then
369
+ local error_count
370
+ error_count=$(jq '[.findings[] | select(.severity=="error")] | length' "$review_file" 2>/dev/null || echo "0")
371
+ if [ "$error_count" -gt 0 ]; then
372
+ fail "code-review.json 中发现 $error_count 个 error,必须修复"
373
+ fi
374
+ pass "Review errors: 0"
375
+ fi
376
+
377
+ write_gate_passed "phases.code_review.gates.lint_gate"
378
+ pass "Lint Gate 全部通过"
379
+ }
380
+
381
+ # ===== 测试框架自动检测 =====
382
+ # 优先级:.pipeline-state.json 中的 test_framework > 项目配置文件检测 > 默认 pytest
383
+ detect_test_framework() {
384
+ framework="" test_cmd="" coverage_cmd=""
385
+
386
+ # 1. 先尝试从状态文件读取 test-writer 的检测结果
387
+ if [ -f "$STATE_FILE" ]; then
388
+ local recorded
389
+ recorded=$(jq -r '.phases.test.framework // empty' "$STATE_FILE" 2>/dev/null)
390
+ if [ -n "$recorded" ]; then
391
+ framework="$recorded"
392
+ fi
393
+ fi
394
+
395
+ # 2. 如果状态文件没有,从项目配置文件检测
396
+ if [ -z "$framework" ]; then
397
+ if [ -f "$PROJECT_ROOT/package.json" ]; then
398
+ if grep -q '"jest"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
399
+ framework="jest"
400
+ elif grep -q '"vitest"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
401
+ framework="vitest"
402
+ elif grep -q '"mocha"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
403
+ framework="mocha"
404
+ fi
405
+ elif [ -f "$PROJECT_ROOT/go.mod" ]; then
406
+ framework="go-test"
407
+ elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then
408
+ framework="cargo-test"
409
+ elif [ -f "$PROJECT_ROOT/pom.xml" ] || [ -f "$PROJECT_ROOT/build.gradle" ]; then
410
+ framework="junit"
411
+ fi
412
+ fi
413
+
414
+ # 3. 默认兜底
415
+ if [ -z "$framework" ]; then
416
+ framework="pytest"
417
+ fi
418
+
419
+ # 根据框架确定命令
420
+ case "$framework" in
421
+ jest)
422
+ test_cmd="npx jest"
423
+ coverage_cmd="npx jest --coverage"
424
+ ;;
425
+ vitest)
426
+ test_cmd="npx vitest run"
427
+ coverage_cmd="npx vitest run --coverage"
428
+ ;;
429
+ mocha)
430
+ test_cmd="npx mocha"
431
+ coverage_cmd="npx nyc mocha"
432
+ ;;
433
+ go-test)
434
+ test_cmd="go test"
435
+ coverage_cmd="go test -cover"
436
+ ;;
437
+ cargo-test)
438
+ test_cmd="cargo test"
439
+ coverage_cmd="cargo tarpaulin 2>/dev/null || cargo test" # tarpaulin 可能未安装
440
+ ;;
441
+ junit)
442
+ if [ -f "$PROJECT_ROOT/pom.xml" ]; then
443
+ test_cmd="mvn test"
444
+ coverage_cmd="mvn jacoco:report"
445
+ else
446
+ test_cmd="gradle test"
447
+ coverage_cmd="gradle jacocoTestReport"
448
+ fi
449
+ ;;
450
+ pytest|*)
451
+ test_cmd="pytest"
452
+ coverage_cmd="pytest --cov --cov-fail-under=80"
453
+ ;;
454
+ esac
455
+
456
+ echo "检测到测试框架: $framework (命令: $test_cmd)"
457
+ }
458
+
459
+ gate_test_unit() {
460
+ echo "正在执行单元测试..."
461
+ detect_test_framework
462
+
463
+ # 确定测试目录
464
+ local test_dir=""
465
+ for d in "$PROJECT_ROOT/tests/unit" "$PROJECT_ROOT/tests" "$PROJECT_ROOT/__tests__" "$PROJECT_ROOT/test"; do
466
+ if [ -d "$d" ]; then
467
+ test_dir="$d"
468
+ break
469
+ fi
470
+ done
471
+
472
+ if [ -z "$test_dir" ]; then
473
+ echo "⚠️ 未找到测试目录,尝试用框架默认命令运行..."
474
+ if ! eval "$test_cmd" 2>&1; then
475
+ fail "单元测试失败(无测试目录且框架命令执行失败)"
476
+ fi
477
+ else
478
+ echo "测试目录: $test_dir"
479
+ if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
480
+ fail "单元测试失败"
481
+ fi
482
+ fi
483
+
484
+ # 覆盖率检查(非阻塞,警告即可——覆盖率的深入分析由 test-runner agent 负责)
485
+ echo "正在检查覆盖率..."
486
+ if ! eval "$coverage_cmd \"$test_dir\" 2>&1"; then
487
+ echo "⚠️ 覆盖率检查未通过(不阻塞,由 test-runner agent 深入分析)"
488
+ fi
489
+
490
+ write_gate_passed "phases.test.sub_phases.unit.gates.test_unit_gate"
491
+ pass "单元测试通过"
492
+ }
493
+
494
+ gate_test_integration() {
495
+ echo "正在执行集成测试..."
496
+ detect_test_framework
497
+
498
+ local test_dir=""
499
+ for d in "$PROJECT_ROOT/tests/integration" "$PROJECT_ROOT/__tests__/integration"; do
500
+ if [ -d "$d" ]; then
501
+ test_dir="$d"
502
+ break
503
+ fi
504
+ done
505
+
506
+ if [ -z "$test_dir" ]; then
507
+ echo "⚠️ 无集成测试目录,跳过"
508
+ else
509
+ echo "测试目录: $test_dir"
510
+ if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
511
+ fail "集成测试失败"
512
+ fi
513
+ # 覆盖率
514
+ if ! eval "$coverage_cmd \"$test_dir\" 2>&1"; then
515
+ echo "⚠️ 覆盖率检查未通过(不阻塞)"
516
+ fi
517
+ fi
518
+ write_gate_passed "phases.test.sub_phases.integration.gates.test_integration_gate"
519
+ pass "集成测试通过"
520
+ }
521
+
522
+ gate_test_e2e() {
523
+ echo "正在执行 E2E 测试..."
524
+ detect_test_framework
525
+
526
+ local test_dir=""
527
+ for d in "$PROJECT_ROOT/tests/e2e" "$PROJECT_ROOT/__tests__/e2e" "$PROJECT_ROOT/e2e"; do
528
+ if [ -d "$d" ]; then
529
+ test_dir="$d"
530
+ break
531
+ fi
532
+ done
533
+
534
+ if [ -z "$test_dir" ]; then
535
+ echo "⚠️ 无 E2E 测试目录,跳过"
536
+ else
537
+ echo "测试目录: $test_dir"
538
+ if ! eval "$test_cmd \"$test_dir\" -v 2>&1"; then
539
+ fail "E2E 测试失败"
540
+ fi
541
+ # E2E 通常不需要覆盖率检查
542
+ fi
543
+ write_gate_passed "phases.test.sub_phases.e2e.gates.test_e2e_gate"
544
+ pass "E2E 测试通过"
545
+ }
546
+
547
+ gate_archive() {
548
+ if [ -z "$CHANGE" ]; then
549
+ fail "需要 --change <name>"
550
+ fi
551
+
552
+ # 如果传了 --execute,执行实际归档动作
553
+ if [ "${1:-}" = "--execute" ]; then
554
+ local src_dir="$PROJECT_ROOT/specline/changes/$CHANGE"
555
+ local archive_dir="$PROJECT_ROOT/specline/changes/archive"
556
+ local date_prefix
557
+ date_prefix=$(date -u +"%Y-%m-%d")
558
+ local dest="$archive_dir/${date_prefix}-${CHANGE}"
559
+
560
+ if [ ! -d "$src_dir" ]; then
561
+ fail "Change '$CHANGE' 不存在: $src_dir"
562
+ fi
563
+
564
+ # 检查基本文件
565
+ if [ ! -f "$src_dir/proposal.md" ]; then
566
+ fail "缺少 proposal.md"
567
+ fi
568
+ if [ ! -f "$src_dir/tasks.md" ]; then
569
+ fail "缺少 tasks.md"
570
+ fi
571
+
572
+ # 同步 delta specs 到主 specs
573
+ if [ -d "$src_dir/specs" ]; then
574
+ echo "正在同步 delta specs 到 specline/specs/..."
575
+ cp -r "$src_dir/specs/"* "$PROJECT_ROOT/specline/specs/" 2>/dev/null || true
576
+ fi
577
+
578
+ # 移动到归档
579
+ mkdir -p "$archive_dir"
580
+ if [ -d "$dest" ]; then
581
+ fail "归档目标已存在: $dest"
582
+ fi
583
+
584
+ mv "$src_dir" "$dest"
585
+ echo "✅ 已归档到: $dest"
586
+
587
+ # 更新状态文件
588
+ if [ -f "$dest/.pipeline-state.json" ]; then
589
+ local now
590
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
591
+ # macOS sed 兼容
592
+ sed -i '' "s/\"current_phase\": \"[^\"]*\"/\"current_phase\": \"archived\"/g" "$dest/.pipeline-state.json" 2>/dev/null || true
593
+ fi
594
+
595
+ exit 0
596
+ fi
597
+
598
+ # 验证模式(原有逻辑,路径改为 specline)
599
+ local archive_dir="$PROJECT_ROOT/specline/changes/archive"
600
+ local found
601
+ found=$(find "$archive_dir" -maxdepth 1 -type d -name "*$CHANGE" 2>/dev/null | head -1)
602
+
603
+ if [ -z "$found" ]; then
604
+ fail "归档目录不存在: $archive_dir/*$CHANGE"
605
+ fi
606
+
607
+ if [ ! -f "$found/proposal.md" ]; then
608
+ fail "归档目录缺少 proposal.md"
609
+ fi
610
+ if [ ! -f "$found/tasks.md" ]; then
611
+ fail "归档目录缺少 tasks.md"
612
+ fi
613
+
614
+ write_gate_passed "phases.archive.gates.archive_gate"
615
+ pass "Archive Gate 全部通过"
616
+ }
617
+
618
+ gate_status() {
619
+ if [ ! -f "$STATE_FILE" ]; then
620
+ echo '{"status":"no_pipeline","message":"未找到流水线状态文件"}'
621
+ exit 0
622
+ fi
623
+
624
+ jq '{
625
+ change: .change_name,
626
+ phase: .current_phase,
627
+ step: .current_step,
628
+ tasks: .phases.coding.tasks | map({id: .id, type: .type, status: .status, batch: .batch}),
629
+ progress: {
630
+ spec: .phases.spec.status,
631
+ coding: .phases.coding.status,
632
+ code_review: .phases.code_review.status,
633
+ test: .phases.test.status,
634
+ archive: .phases.archive.status
635
+ }
636
+ }' "$STATE_FILE"
637
+ }
638
+
639
+ # ===== 分派 =====
640
+
641
+ case "$PHASE" in
642
+ new)
643
+ gate_new
644
+ ;;
645
+ list)
646
+ gate_list "$@"
647
+ ;;
648
+ artifacts)
649
+ gate_artifacts "$@"
650
+ ;;
651
+ spec)
652
+ gate_spec
653
+ ;;
654
+ build)
655
+ gate_build
656
+ ;;
657
+ lint)
658
+ gate_lint
659
+ ;;
660
+ test-unit)
661
+ gate_test_unit
662
+ ;;
663
+ test-integration)
664
+ gate_test_integration
665
+ ;;
666
+ test-e2e)
667
+ gate_test_e2e
668
+ ;;
669
+ archive)
670
+ gate_archive "$@"
671
+ ;;
672
+ status)
673
+ gate_status
674
+ ;;
675
+ *)
676
+ echo "未知 phase: $PHASE"
677
+ echo "可用: new | list | artifacts | spec | build | lint | test-unit | test-integration | test-e2e | archive | status"
678
+ exit 2
679
+ ;;
680
+ esac
681
+
682
+ exit 0
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # shell-guard.sh — beforeShellExecution Hook: 拦截危险命令
3
+ input=$(cat)
4
+ command=$(echo "$input" | jq -r '.command // empty')
5
+ if echo "$command" | grep -qE "rm -rf|rm -r "; then
6
+ echo '{"permission": "deny", "user_message": "危险命令被拦截: rm -rf"}'
7
+ exit 0
8
+ fi
9
+ if echo "$command" | grep -qE "curl.*\|.*bash|wget.*\|.*sh"; then
10
+ echo '{"permission": "deny", "user_message": "危险命令被拦截: 管道执行远程脚本"}'
11
+ exit 0
12
+ fi
13
+ if echo "$command" | grep -qE "^sudo "; then
14
+ echo '{"permission": "deny", "user_message": "sudo 需要人工审批"}'
15
+ exit 0
16
+ fi
17
+ echo '{"permission": "allow"}'
18
+ exit 0