feat: 添加 dev-workflow 和 pr-review-gate 技能

## dev-workflow
- Spec-Driven 开发流程
- 从需求生成 spec.md
- 创建功能分支
- 调用子代理开发
- 自动创建 Gitea PR

## pr-review-gate
- 3 审查者并行审查(安全/质量/逻辑)
- 自动合并逻辑
- 修复循环(最多3次)

## .planning
- PROJECT.md: 项目定义
- REQUIREMENTS.md: 需求列表
- ROADMAP.md: 开发路线图
- STATE.md: 当前状态
This commit is contained in:
2026-03-09 01:22:28 +08:00
parent 3850bc33dc
commit 33c8de02da
12 changed files with 1501 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
---
name: pr-review-gate
description: "PR 审查门禁技能。当 PR 创建后自动触发,启动 3 个审查子代理并行审查。"
---
# pr-review-gate - PR 审查门禁
## 什么时候使用
- PR 创建后自动触发
- 用户说"审查 PR #xxx"
- 用户说"开始审查"
## 环境变量
| 变量名 | 用途 | 必需 |
|--------|------|------|
| `GITEA_TOKEN` | Gitea API Token | 是 |
| `GITEA_URL` | Gitea 服务地址 | 否 |
| `GITEA_REPO` | 仓库路径 | 否 |
## 工作流程
```
PR 创建
↓
┌─────────────────────────────────────────────────────┐
│ 并行启动 3 个审查子代理 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Security │ │ Quality │ │ Logic │ │
│ │ Reviewer │ │ Reviewer │ │ Reviewer │ │
│ │ (安全) │ │ (质量) │ │ (逻辑) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ approve? approve? approve? │
│ │ │ │ │
└─────────┼───────────────┼───────────────┼───────────┘
│ │ │
└───────────────┼───────────────┘
▼
┌───────────────────────┐
│ 收集审查结果 │
└───────────┬───────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
全部 approve 有 reject CI 失败
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 自动合并 │ │ 修复循环 │ │ 等待修复 │
│ 到 main │ │ (最多3次) │ │ │
└───────────┘ └───────────┘ └───────────┘
```
## 审查者角色
### 1. Security Reviewer (安全审查者)
**关注点:**
- 输入验证
- SQL 注入 / XSS / CSRF
- 敏感数据处理
- 权限检查
- 依赖安全
**审查清单:**
- [ ] 所有用户输入都经过验证和清理
- [ ] 没有硬编码的密钥或凭据
- [ ] 文件操作有路径检查
- [ ] API 调用有适当的认证
### 2. Quality Reviewer (代码质量审查者)
**关注点:**
- 代码可读性
- 命名规范
- 代码结构
- 测试覆盖
- 文档完整性
**审查清单:**
- [ ] 函数/变量命名清晰
- [ ] 代码有适当的注释
- [ ] 复杂逻辑有文档说明
- [ ] 有单元测试覆盖
- [ ] SKILL.md 已更新(如适用)
### 3. Logic Reviewer (业务逻辑审查者)
**关注点:**
- 需求是否满足
- 边界条件处理
- 错误处理
- 与现有功能的兼容性
- 性能影响
**审查清单:**
- [ ] 实现符合 spec 描述
- [ ] 边界条件已处理
- [ ] 错误情况有适当的处理
- [ ] 不会破坏现有功能
- [ ] 性能可接受
## 执行步骤
### Step 1: 获取 PR 信息
```bash
python3 pr_gate.py --get-pr --number 123
```
获取 PR 的:
- 标题和描述
- 变更文件列表
- diff 内容
### Step 2: 启动审查子代理
使用 `sessions_spawn` 并行启动 3 个审查子代理:
```
# Security Reviewer
sessions_spawn(
task="审查 PR #xxx 的安全性",
label="security-reviewer",
runtime="subagent"
)
# Quality Reviewer
sessions_spawn(
task="审查 PR #xxx 的代码质量",
label="quality-reviewer",
runtime="subagent"
)
# Logic Reviewer
sessions_spawn(
task="审查 PR #xxx 的业务逻辑",
label="logic-reviewer",
runtime="subagent"
)
```
### Step 3: 收集审查结果
等待所有子代理完成,收集结果:
```json
{
"security": {
"reviewer": "security",
"result": "approve|reject",
"comments": ["..."],
"issues": []
},
"quality": {
"reviewer": "quality",
"result": "approve",
"comments": ["..."],
"issues": []
},
"logic": {
"reviewer": "logic",
"result": "approve",
"comments": ["..."],
"issues": []
}
}
```
### Step 4: 决策
**全部 approve + CI 通过:**
```bash
python3 pr_gate.py --merge-pr --number 123
```
**有 reject:**
```bash
python3 pr_gate.py --fix-loop --number 123 --attempt 1
```
### Step 5: 修复循环(最多 3 次)
1. 汇总所有问题
2. 启动修复子代理
3. 修复后重新审查
4. 3 次后仍有问题 → 汇报失败
## CLI 命令
```bash
# 获取 PR 信息
python3 pr_gate.py --get-pr --number 123
# 启动审查
python3 pr_gate.py --review --number 123
# 检查审查结果
python3 pr_gate.py --check-results --number 123
# 合并 PR
python3 pr_gate.py --merge-pr --number 123
# 修复循环
python3 pr_gate.py --fix-loop --number 123 --attempt 1
# 完整流程
python3 pr_gate.py --full-pipeline --number 123
```
## 输出格式
### 审查通过
```
✅ PR #123 审查通过
**审查结果:**
| 审查者 | 结果 | 评论数 |
|--------|------|--------|
| Security | ✅ approve | 0 |
| Quality | ✅ approve | 2 |
| Logic | ✅ approve | 1 |
**CI 状态:** ✅ 通过
正在合并到 main...
```
### 需要修复
```
⚠️ PR #123 需要修复
**问题汇总:**
| 审查者 | 问题 |
|--------|------|
| Security | 输入验证缺失 (HIGH) |
| Quality | 缺少单元测试 (MEDIUM) |
启动修复流程 (尝试 1/3)...
```
### 最终失败
```
❌ PR #123 修复失败
经过 3 次修复尝试,以下问题仍未解决:
1. [Security] 输入验证缺失
- 位置: dev_workflow.py:45
- 描述: 用户输入未经过验证直接使用
请人工介入处理。
```
## 与其他技能的协作
| 技能 | 协作方式 |
|------|----------|
| **dev-workflow** | PR 创建后触发审查 |
| **Gitea CI** | 等待 CI 通过后才合并 |
## 注意事项
1. 审查子代理必须独立运行,互不干扰
2. 修复循环有明确的上限(3 次)
3. 最终失败必须汇报详细问题
4. 合并前必须确认 CI 通过
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pr_gate.py - PR 审查门禁辅助工具
用法:
python3 pr_gate.py --get-pr --number 123
python3 pr_gate.py --review --number 123
python3 pr_gate.py --check-results --number 123
python3 pr_gate.py --merge-pr --number 123
python3 pr_gate.py --full-pipeline --number 123
"""
import os
import sys
import json
import argparse
import subprocess
import time
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional
# 配置
GITEA_URL = os.environ.get('GITEA_URL', 'http://192.168.1.22:8418')
GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '')
GITEA_REPO = os.environ.get('GITEA_REPO', 'starmen1265/kb-skills')
REPO_PATH = Path.home() / 'work' / 'kb-skills'
PLANNING_DIR = REPO_PATH / '.planning'
MAX_FIX_ATTEMPTS = 3
def run_cmd(cmd: str, cwd=None, capture=True) -> subprocess.CompletedProcess:
"""执行 shell 命令"""
return subprocess.run(
cmd,
shell=True,
cwd=cwd or REPO_PATH,
capture_output=capture,
text=True
)
def gitea_api(method: str, endpoint: str, data: dict = None) -> dict:
"""调用 Gitea API"""
import urllib.request
import urllib.error
url = f"{GITEA_URL}/api/v1{endpoint}"
headers = {
'Authorization': f'token {GITEA_TOKEN}',
'Content-Type': 'application/json'
}
if data:
body = json.dumps(data).encode('utf-8')
else:
body = None
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as response:
if response.status == 204:
return {"success": True}
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else str(e)
return {"success": False, "error": f"HTTP {e.code}: {error_body}"}
except Exception as e:
return {"success": False, "error": str(e)}
def get_pr(number: int) -> dict:
"""获取 PR 信息"""
result = gitea_api('GET', f'/repos/{GITEA_REPO}/pulls/{number}')
if 'error' in result:
return {"success": False, "error": result['error']}
# 获取变更文件
files_result = gitea_api('GET', f'/repos/{GITEA_REPO}/pulls/{number}/files')
# 获取 CI 状态
commits_result = gitea_api('GET', f'/repos/{GITEA_REPO}/pulls/{number}/commits')
return {
"success": True,
"pr": {
"number": result.get("number"),
"title": result.get("title"),
"body": result.get("body"),
"state": result.get("state"),
"head": result.get("head", {}).get("ref"),
"base": result.get("base", {}).get("ref"),
"mergeable": result.get("mergeable"),
"user": result.get("user", {}).get("login"),
"created_at": result.get("created_at"),
"url": result.get("html_url")
},
"files": files_result if isinstance(files_result, list) else [],
"ci_status": _extract_ci_status(commits_result)
}
def _extract_ci_status(commits: list) -> dict:
"""从 commits 提取 CI 状态"""
# 简化实现,实际需要检查 commit status
return {
"status": "unknown",
"checks": []
}
def start_review(number: int) -> dict:
"""启动 3 个审查子代理"""
# 获取 PR 信息
pr_info = get_pr(number)
if not pr_info.get("success"):
return pr_info
# 这里应该使用 sessions_spawn 启动子代理
# 由于当前环境限制,返回模拟结果
return {
"success": True,
"message": f"已启动 3 个审查子代理审查 PR #{number}",
"reviewers": ["security", "quality", "logic"],
"pr": pr_info["pr"],
"note": "请在 OpenClaw 环境中使用 sessions_spawn 启动实际子代理"
}
def check_results(number: int) -> dict:
"""检查审查结果"""
results_file = PLANNING_DIR / f'review-pr-{number}.json'
if not results_file.exists():
return {
"success": True,
"status": "pending",
"message": "审查尚未完成"
}
results = json.loads(results_file.read_text(encoding='utf-8'))
# 统计结果
approve_count = sum(1 for r in results.get("reviews", []) if r.get("result") == "approve")
reject_count = sum(1 for r in results.get("reviews", []) if r.get("result") == "reject")
all_approved = approve_count == 3
any_rejected = reject_count > 0
return {
"success": True,
"status": "complete",
"all_approved": all_approved,
"any_rejected": any_rejected,
"results": results
}
def merge_pr(number: int) -> dict:
"""合并 PR"""
# 检查审查结果
review_status = check_results(number)
if not review_status.get("all_approved"):
return {
"success": False,
"error": "审查未全部通过",
"review_status": review_status
}
# 调用 Gitea API 合并
result = gitea_api('POST', f'/repos/{GITEA_REPO}/pulls/{number}/merge', {
"Do": "merge",
"merge_title": f"Merge PR #{number}",
"merge_message": ""
})
if result.get("success"):
return {
"success": True,
"message": f"PR #{number} 已合并到 main",
"pr_number": number
}
else:
return result
def fix_loop(number: int, attempt: int = 1) -> dict:
"""修复循环"""
if attempt > MAX_FIX_ATTEMPTS:
return {
"success": False,
"error": f"已达到最大修复次数 ({MAX_FIX_ATTEMPTS})",
"final_failure": True
}
# 获取需要修复的问题
review_status = check_results(number)
issues = []
for review in review_status.get("results", {}).get("reviews", []):
if review.get("result") == "reject":
issues.extend(review.get("issues", []))
if not issues:
return {
"success": True,
"message": "没有需要修复的问题"
}
return {
"success": True,
"attempt": attempt,
"max_attempts": MAX_FIX_ATTEMPTS,
"issues_to_fix": issues,
"message": f"修复尝试 {attempt}/{MAX_FIX_ATTEMPTS}",
"note": "请在 OpenClaw 环境中启动修复子代理"
}
def full_pipeline(number: int) -> dict:
"""完整审查流程"""
steps = []
# Step 1: 获取 PR
pr_info = get_pr(number)
steps.append({"step": "get_pr", "success": pr_info.get("success", False)})
if not pr_info.get("success"):
return {"success": False, "steps": steps, "error": pr_info.get("error")}
# Step 2: 启动审查
review_result = start_review(number)
steps.append({"step": "start_review", "success": review_result.get("success", False)})
# Step 3: 等待审查完成 (模拟)
# 实际应该轮询 check_results
steps.append({"step": "wait_review", "success": True, "note": "审查进行中..."})
# Step 4: 检查结果
results = check_results(number)
steps.append({"step": "check_results", "success": True, "data": results})
if results.get("all_approved"):
# Step 5a: 合并
merge_result = merge_pr(number)
steps.append({"step": "merge", "success": merge_result.get("success", False)})
return {"success": True, "steps": steps, "merged": merge_result.get("success", False)}
else:
# Step 5b: 修复循环
fix_result = fix_loop(number, attempt=1)
steps.append({"step": "fix_loop", "success": False, "data": fix_result})
return {"success": False, "steps": steps, "needs_fix": True}
def save_review_results(number: int, reviews: List[dict]) -> dict:
"""保存审查结果"""
PLANNING_DIR.mkdir(parents=True, exist_ok=True)
results_file = PLANNING_DIR / f'review-pr-{number}.json'
data = {
"pr_number": number,
"timestamp": datetime.now().isoformat(),
"reviews": reviews
}
results_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
return {"success": True, "file": str(results_file)}
def main():
parser = argparse.ArgumentParser(description='pr-review-gate 辅助工具')
subparsers = parser.add_subparsers(dest='command', help='命令')
# get-pr
get_parser = subparsers.add_parser('get-pr', help='获取 PR 信息')
get_parser.add_argument('--number', type=int, required=True, help='PR 编号')
# review
review_parser = subparsers.add_parser('review', help='启动审查')
review_parser.add_argument('--number', type=int, required=True, help='PR 编号')
# check-results
check_parser = subparsers.add_parser('check-results', help='检查审查结果')
check_parser.add_argument('--number', type=int, required=True, help='PR 编号')
# merge-pr
merge_parser = subparsers.add_parser('merge-pr', help='合并 PR')
merge_parser.add_argument('--number', type=int, required=True, help='PR 编号')
# fix-loop
fix_parser = subparsers.add_parser('fix-loop', help='修复循环')
fix_parser.add_argument('--number', type=int, required=True, help='PR 编号')
fix_parser.add_argument('--attempt', type=int, default=1, help='尝试次数')
# full-pipeline
full_parser = subparsers.add_parser('full-pipeline', help='完整流程')
full_parser.add_argument('--number', type=int, required=True, help='PR 编号')
# save-results
save_parser = subparsers.add_parser('save-results', help='保存审查结果')
save_parser.add_argument('--number', type=int, required=True, help='PR 编号')
save_parser.add_argument('--reviews', type=str, required=True, help='审查结果 JSON')
args = parser.parse_args()
if args.command == 'get-pr':
result = get_pr(args.number)
elif args.command == 'review':
result = start_review(args.number)
elif args.command == 'check-results':
result = check_results(args.number)
elif args.command == 'merge-pr':
result = merge_pr(args.number)
elif args.command == 'fix-loop':
result = fix_loop(args.number, args.attempt)
elif args.command == 'full-pipeline':
result = full_pipeline(args.number)
elif args.command == 'save-results':
result = save_review_results(args.number, json.loads(args.reviews))
else:
parser.print_help()
return
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
+62
View File
@@ -0,0 +1,62 @@
# Logic Reviewer 指南
## 角色
你是一个业务逻辑审查者,负责审查 PR 是否满足需求。
## 审查重点
### 1. 需求满足
- 实现符合 spec 描述
- 所有需求都有覆盖
- 验收标准已达成
### 2. 边界条件
- 空值处理
- 极端情况处理
- 错误输入处理
### 3. 错误处理
- 异常被正确捕获
- 错误信息有意义
- 不会静默失败
### 4. 兼容性
- 不破坏现有功能
- 接口变更合理
- 向后兼容
### 5. 性能
- 没有明显的性能问题
- 资源使用合理
## 输出格式
```json
{
"reviewer": "logic",
"result": "approve|reject",
"requirements_check": {
"REQ-01": {"status": "done", "note": "已实现"},
"REQ-02": {"status": "partial", "note": "缺少边界处理"},
"REQ-03": {"status": "missing", "note": "未实现"}
},
"issues": [
{
"type": "edge-case",
"severity": "medium",
"description": "空列表输入未处理",
"location": "dev_workflow.py:78",
"suggestion": "添加空列表检查"
}
],
"comments": [
"实现基本完整,建议补充边界条件测试"
]
}
```
## 决策规则
- **approve**: 需求满足,边界处理合理
- **reject**: 需求未满足,或存在严重的逻辑问题
+54
View File
@@ -0,0 +1,54 @@
# Quality Reviewer 指南
## 角色
你是一个代码质量审查者,负责审查 PR 的代码质量。
## 审查重点
### 1. 代码可读性
- 命名清晰有意义
- 函数长度适中
- 逻辑清晰易懂
### 2. 代码结构
- 单一职责原则
- 模块化设计
- 减少重复代码
### 3. 测试覆盖
- 有单元测试
- 边界条件测试
- 错误路径测试
### 4. 文档
- 函数有文档字符串
- 复杂逻辑有注释
- SKILL.md 已更新
## 输出格式
```json
{
"reviewer": "quality",
"result": "approve|reject",
"issues": [
{
"file": "path/to/file.py",
"line": 23,
"type": "naming",
"severity": "low",
"description": "变量名 x 不够清晰",
"suggestion": "使用更有意义的名称如 user_count"
}
],
"comments": [
"建议:将这个长函数拆分成更小的函数"
]
}
```
## 决策规则
- **approve**: 代码质量良好,或仅有小的改进建议
- **reject**: 存在严重的质量问题(如缺少测试、代码无法理解)
+55
View File
@@ -0,0 +1,55 @@
# Security Reviewer 指南
## 角色
你是一个安全审查者,负责审查 PR 的安全性。
## 审查重点
### 1. 输入验证
- 所有用户输入都经过验证
- 使用参数化查询(SQL)
- 对输出进行编码(XSS)
- 文件路径有检查
### 2. 认证授权
- API 有适当的认证
- 权限检查存在
- 敏感操作有验证
### 3. 敏感数据
- 没有硬编码密钥
- 日志不泄露敏感信息
- 配置文件安全
### 4. 依赖安全
- 依赖版本安全
- 没有已知漏洞
## 输出格式
```json
{
"reviewer": "security",
"result": "approve|reject",
"severity": "high|medium|low|none",
"issues": [
{
"file": "path/to/file.py",
"line": 45,
"type": "input-validation",
"severity": "high",
"description": "用户输入未验证",
"suggestion": "添加输入验证逻辑"
}
],
"comments": [
"建议:考虑使用环境变量存储敏感配置"
]
}
```
## 决策规则
- **approve**: 无安全问题,或仅有低风险建议
- **reject**: 存在中/高风险安全问题