diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 0000000..5da6fa2 --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,36 @@ +# dev-workflow + pr-review-gate 技能开发 + +## Core Value + +为 kb-skills 仓库提供完整的 Spec-Driven 开发流程,包括: +- 功能分支开发 +- 自动创建 Gitea PR +- 3 审查者并行审查 +- 自动测试 + 自动合并 + +## Context + +### 现有基础设施 +- Gitea Runner 已配置 (`self-hosted`, `ubuntu-latest`) +- CI/CD: `.gitea/workflows/deploy.yml` (部署), `.gitea/workflows/test.yml` (PR 测试) +- 目标仓库: `ssh://gitea-nas/starmen1265/kb-skills.git` + +### 用户工作流 +``` +需求 → Spec → 开发 → PR → 3审查者审查 → 测试 → 合并 → 部署 +``` + +## Constraints + +- 必须与现有 Gitea 环境兼容 +- 审查者使用子代理实现 +- 修复循环最多 3 次 +- 必须集成到 kb-skills 作为新技能 + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| 使用 GSD Spec-Driven | 成熟的开发方法论 | 已安装 GSD | +| 审查者为子代理 | 实现并行审查 | 待开发 | +| Gitea API 自动 PR | 无 GitHub CLI | 待开发 | diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..5873013 --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,51 @@ +# Requirements - dev-workflow + pr-review-gate + +## v1 Requirements + +### dev-workflow 技能 + +- [ ] **DEV-01**: 用户描述需求后,生成 spec.md 文件 +- [ ] **DEV-02**: 从 spec 创建 `feature/*` 功能分支 +- [ ] **DEV-03**: 调用子代理进行开发(基于 GSD 子代理编排) +- [ ] **DEV-04**: 开发完成后自动提交到功能分支 +- [ ] **DEV-05**: 调用 Gitea API 创建 PR +- [ ] **DEV-06**: 触发 CI 测试(通过现有 test.yml) + +### pr-review-gate 技能 + +- [ ] **PRG-01**: PR 创建后自动启动 3 个审查子代理 +- [ ] **PRG-02**: 审查者1 - Security Reviewer(安全审查) +- [ ] **PRG-03**: 审查者2 - Quality Reviewer(代码质量审查) +- [ ] **PRG-04**: 审查者3 - Logic Reviewer(业务逻辑审查) +- [ ] **PRG-05**: 收集 3 个审查结果 +- [ ] **PRG-06**: 全部 approve + CI 通过 → 调用 Gitea API 合并 +- [ ] **PRG-07**: 有 reject → 修复 → 重新审查(最多 3 次) +- [ ] **PRG-08**: 3 次修复后仍有问题 → 汇报失败,等待人工介入 + +### 集成要求 + +- [ ] **INT-01**: 技能安装到 `kb-skills/dev-workflow/` 和 `kb-skills/pr-review-gate/` +- [ ] **INT-02**: 每个技能包含 `SKILL.md` 和 Python 辅助脚本 +- [ ] **INT-03**: 修改 `.gitea/workflows/` 支持自动合并逻辑 + +## v2 Requirements (Future) + +- 支持自定义审查者数量 +- 支持不同类型的审查模板 +- 支持 GitHub 兼容模式 + +## Out of Scope + +- GitHub 原生支持(仅 Gitea) +- 图形化界面 +- 实时协作审查 + +## Traceability + +| Phase | Requirements | +|-------|--------------| +| Phase 1: dev-workflow 核心 | DEV-01, DEV-02, DEV-03, DEV-04 | +| Phase 2: Gitea PR 集成 | DEV-05, DEV-06 | +| Phase 3: pr-review-gate 核心 | PRG-01 ~ PRG-05 | +| Phase 4: 自动合并逻辑 | PRG-06, PRG-07, PRG-08, INT-03 | +| Phase 5: 集成测试 | INT-01, INT-02 | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..8093948 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,106 @@ +# Roadmap - dev-workflow + pr-review-gate + +## Overview + +**5 Phases** | **11 Requirements** | All v1 requirements covered ✓ + +| # | Phase | Goal | Requirements | Success Criteria | +|---|-------|------|--------------|------------------| +| 1 | dev-workflow 核心 | 实现基础开发流程技能 | DEV-01~04 | 用户可以说"添加XXX功能",系统生成 spec 并在 feature 分支开发 | +| 2 | Gitea PR 集成 | 自动创建 PR 和触发测试 | DEV-05~06 | 开发完成后自动创建 PR,CI 测试运行 | +| 3 | pr-review-gate 核心 | 实现 3 审查者并行审查 | PRG-01~05 | PR 创建后自动启动 3 个审查子代理并收集结果 | +| 4 | 自动合并逻辑 | 实现审查通过后自动合并 | PRG-06~08, INT-03 | 全部通过自动合并,失败自动修复循环 | +| 5 | 集成测试 | 完整流程验证 | INT-01~02 | 端到端测试通过,技能可用 | + +--- + +## Phase 1: dev-workflow 核心 + +**Goal:** 实现基础开发流程技能 + +**Requirements:** DEV-01, DEV-02, DEV-03, DEV-04 + +**Deliverables:** +- `kb-skills/dev-workflow/SKILL.md` +- `kb-skills/dev-workflow/dev_workflow.py` +- `kb-skills/dev-workflow/templates/spec-template.md` + +**Success Criteria:** +1. 用户描述需求后,生成 `.planning/spec.md` 文件 +2. 从 spec 创建 `feature/*` 功能分支 +3. 子代理可以基于 spec 进行开发 +4. 开发完成后提交到功能分支 + +--- + +## Phase 2: Gitea PR 集成 + +**Goal:** 自动创建 PR 和触发测试 + +**Requirements:** DEV-05, DEV-06 + +**Deliverables:** +- `dev_workflow.py` 添加 Gitea API 调用 +- 获取 Gitea API Token 配置 + +**Success Criteria:** +1. 开发完成后调用 Gitea API 创建 PR +2. PR 创建后自动触发 CI 测试 +3. 返回 PR 编号和链接 + +--- + +## Phase 3: pr-review-gate 核心 + +**Goal:** 实现 3 审查者并行审查 + +**Requirements:** PRG-01, PRG-02, PRG-03, PRG-04, PRG-05 + +**Deliverables:** +- `kb-skills/pr-review-gate/SKILL.md` +- `kb-skills/pr-review-gate/pr_gate.py` +- `kb-skills/pr-review-gate/reviewers/security.md` +- `kb-skills/pr-review-gate/reviewers/quality.md` +- `kb-skills/pr-review-gate/reviewers/logic.md` + +**Success Criteria:** +1. PR 创建后自动启动 3 个审查子代理 +2. 每个审查者有明确的审查重点 +3. 收集 3 个审查结果(approve/reject + 评论) +4. 审查结果可追溯 + +--- + +## Phase 4: 自动合并逻辑 + +**Goal:** 实现审查通过后自动合并 + +**Requirements:** PRG-06, PRG-07, PRG-08, INT-03 + +**Deliverables:** +- `pr_gate.py` 添加合并逻辑 +- `.gitea/workflows/auto-merge.yml`(可选) +- 修复循环逻辑 + +**Success Criteria:** +1. 全部 approve + CI 通过 → 自动合并到 main +2. 有 reject → 启动修复流程 +3. 修复后重新审查(最多 3 次) +4. 3 次后仍有问题 → 汇报失败详情 + +--- + +## Phase 5: 集成测试 + +**Goal:** 完整流程验证 + +**Requirements:** INT-01, INT-02 + +**Deliverables:** +- 端到端测试文档 +- 使用示例 + +**Success Criteria:** +1. 完整流程可运行:需求 → Spec → 开发 → PR → 审查 → 合并 +2. 技能文档完整(SKILL.md) +3. 用户可以按照文档使用 diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..c838670 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,30 @@ +# Project State + +## Current Phase + +**Phase 1: dev-workflow 核心** + +Status: `in_progress` + +## Progress + +| Phase | Status | Notes | +|-------|--------|-------| +| Phase 1: dev-workflow 核心 | 🔄 In Progress | 开始开发 | +| Phase 2: Gitea PR 集成 | ⏳ Pending | - | +| Phase 3: pr-review-gate 核心 | ⏳ Pending | - | +| Phase 4: 自动合并逻辑 | ⏳ Pending | - | +| Phase 5: 集成测试 | ⏳ Pending | - | + +## Active Context + +- 开发 dev-workflow 技能 +- 目标: DEV-01 ~ DEV-04 +- 产出: SKILL.md + dev_workflow.py + templates/ + +## Session Log + +### 2026-03-09 01:18 +- 安装 GSD +- 创建 PROJECT.md, REQUIREMENTS.md, ROADMAP.md +- 开始 Phase 1 开发 diff --git a/dev-workflow/SKILL.md b/dev-workflow/SKILL.md new file mode 100644 index 0000000..f59357c --- /dev/null +++ b/dev-workflow/SKILL.md @@ -0,0 +1,166 @@ +--- +name: dev-workflow +description: "Spec-Driven 开发流程技能。当用户说'添加功能'、'开发XXX'时使用。" +--- + +# dev-workflow - Spec-Driven 开发流程 + +## 什么时候使用 + +当用户发送包含以下模式的消息时触发: + +| 触发词 | 行为 | +|--------|------| +| `添加功能` + 描述 | 开始 Spec-Driven 开发流程 | +| `开发` + 描述 | 同上 | +| `实现` + 描述 | 同上 | + +## 环境变量 + +| 变量名 | 用途 | 必需 | +|--------|------|------| +| `GITEA_TOKEN` | Gitea API Token(创建 PR) | 是 | +| `GITEA_URL` | Gitea 服务地址 | 否(默认 http://192.168.1.22:8418) | +| `GITEA_REPO` | 仓库路径 | 否(默认 starmen1265/kb-skills) | + +## 工作流程 + +``` +用户描述需求 + ↓ +┌─────────────────────────────────────┐ +│ Phase 1: Spec 生成 │ +│ - 澄清需求 │ +│ - 生成 .planning/spec.md │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Phase 2: 分支创建 │ +│ - 创建 feature/* 分支 │ +│ - 切换到功能分支 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Phase 3: 子代理开发 │ +│ - 基于 spec 启动开发子代理 │ +│ - 子代理执行开发任务 │ +│ - 提交代码到功能分支 │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Phase 4: 创建 PR │ +│ - 调用 Gitea API 创建 PR │ +│ - 返回 PR 编号 │ +└─────────────────────────────────────┘ + ↓ +通知 pr-review-gate 开始审查 +``` + +## 执行步骤 + +### Step 1: 需求澄清 + +使用 GSD 风格的深度提问: + +1. 理解用户要做什么 +2. 探索边界条件 +3. 确认技术约束 +4. 识别依赖关系 + +**示例问题:** +- "这个功能解决什么问题?" +- "期望的输入/输出是什么?" +- "有什么技术约束?" +- "需要与现有哪些模块交互?" + +### Step 2: 生成 Spec + +创建 `.planning/spec.md` 文件: + +```bash +python3 dev_workflow.py --create-spec --title "功能名称" --description "详细描述" +``` + +Spec 文件结构参考 `templates/spec-template.md`。 + +### Step 3: 创建功能分支 + +```bash +python3 dev_workflow.py --create-branch --spec .planning/spec.md +``` + +分支命名规则: +- `feature/<简短描述>` - 新功能 +- `fix/<简短描述>` - Bug 修复 +- `refactor/<简短描述>` - 重构 + +### Step 4: 启动开发子代理 + +使用 `sessions_spawn` 启动开发子代理: + +``` +sessions_spawn( + task="根据 spec 开发 <功能名称>", + cwd="~/work/kb-skills", + runtime="subagent" +) +``` + +子代理会: +1. 读取 spec 文件 +2. 执行开发任务 +3. 提交代码到功能分支 + +### Step 5: 创建 PR + +开发完成后: + +```bash +python3 dev_workflow.py --create-pr --branch feature/xxx --title "PR 标题" +``` + +调用 Gitea API 创建 PR,返回 PR 编号。 + +### Step 6: 通知审查 + +PR 创建后,告知用户: + +``` +✅ PR #xxx 已创建 + +**分支:** feature/xxx +**链接:** http://192.168.1.22:8418/starmen1265/kb-skills/pulls/xxx + +正在启动 pr-review-gate 审查流程... +``` + +## CLI 命令 + +```bash +# 创建 spec +python3 dev_workflow.py --create-spec --title "标题" --description "描述" + +# 从 spec 创建分支 +python3 dev_workflow.py --create-branch --spec .planning/spec.md + +# 创建 PR +python3 dev_workflow.py --create-pr --branch feature/xxx --title "PR标题" + +# 查看当前状态 +python3 dev_workflow.py --status +``` + +## 与其他技能的协作 + +| 技能 | 协作方式 | +|------|----------| +| **pr-review-gate** | PR 创建后自动触发审查 | +| **kb-workflow** | 可复用其目录结构 | +| **GSD** | 采用其 Spec-Driven 方法论 | + +## 注意事项 + +1. 确保 `GITEA_TOKEN` 已配置 +2. 功能分支命名遵循规范 +3. 提交信息遵循 Conventional Commits +4. 开发前先 pull 最新代码 diff --git a/dev-workflow/dev_workflow.py b/dev-workflow/dev_workflow.py new file mode 100644 index 0000000..5fb6bfd --- /dev/null +++ b/dev-workflow/dev_workflow.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +dev_workflow.py - Spec-Driven 开发流程辅助工具 + +用法: + python3 dev_workflow.py --create-spec --title "标题" --description "描述" + python3 dev_workflow.py --create-branch --spec .planning/spec.md + python3 dev_workflow.py --create-pr --branch feature/xxx --title "PR标题" + python3 dev_workflow.py --status +""" + +import os +import sys +import json +import argparse +import subprocess +import re +from datetime import datetime +from pathlib import Path + + +# 配置 +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' + + +def run_cmd(cmd, cwd=None, capture=True): + """执行 shell 命令""" + result = subprocess.run( + cmd, + shell=True, + cwd=cwd or REPO_PATH, + capture_output=capture, + text=True + ) + return result + + +def create_spec(title: str, description: str) -> dict: + """创建 spec 文件""" + PLANNING_DIR.mkdir(parents=True, exist_ok=True) + + # 生成分支名 + branch_name = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') + + # 读取模板 + template_path = Path(__file__).parent / 'templates' / 'spec-template.md' + if template_path.exists(): + content = template_path.read_text(encoding='utf-8') + else: + content = """# Spec: {{TITLE}} + +**创建时间:** {{DATE}} +**状态:** draft +**分支:** feature/{{BRANCH_NAME}} + +--- + +## 概述 + +{{DESCRIPTION}} + +## 需求 + +### 功能需求 + +- [ ] **REQ-01**: 待补充 + +## 验收标准 + +1. 待补充 + +--- + +## 变更日志 + +| 日期 | 变更 | +|------|------| +| {{DATE}} | 初始创建 | +""" + + # 替换变量 + now = datetime.now().strftime('%Y-%m-%d %H:%M') + content = content.replace('{{TITLE}}', title) + content = content.replace('{{DATE}}', now) + content = content.replace('{{BRANCH_NAME}}', branch_name) + content = content.replace('{{DESCRIPTION}}', description) + + # 写入文件 + spec_file = PLANNING_DIR / 'spec.md' + spec_file.write_text(content, encoding='utf-8') + + return { + "success": True, + "spec_file": str(spec_file), + "branch_name": branch_name, + "title": title + } + + +def create_branch(spec_file: str) -> dict: + """从 spec 创建功能分支""" + spec_path = Path(spec_file) + if not spec_path.exists(): + return {"success": False, "error": f"Spec 文件不存在: {spec_file}"} + + # 从 spec 提取分支名 + content = spec_path.read_text(encoding='utf-8') + match = re.search(r'\*\*分支:\*\*\s*feature/([^\n]+)', content) + if match: + branch_suffix = match.group(1).strip() + else: + branch_suffix = f"feature-{datetime.now().strftime('%Y%m%d%H%M%S')}" + + branch_name = f"feature/{branch_suffix}" + + # 确保在 main 分支 + run_cmd('git checkout main') + run_cmd('git pull origin main') + + # 创建功能分支 + result = run_cmd(f'git checkout -b {branch_name}') + if result.returncode != 0: + # 分支可能已存在,尝试切换 + result = run_cmd(f'git checkout {branch_name}') + if result.returncode != 0: + return {"success": False, "error": f"无法创建分支: {result.stderr}"} + + return { + "success": True, + "branch": branch_name, + "spec_file": spec_file + } + + +def create_pr(branch: str, title: str, body: str = "") -> dict: + """调用 Gitea API 创建 PR""" + if not GITEA_TOKEN: + return {"success": False, "error": "GITEA_TOKEN 未配置"} + + # 获取最新提交信息作为 body(如果未提供) + if not body: + result = run_cmd('git log -1 --pretty=%B') + body = result.stdout.strip() if result.returncode == 0 else "" + + # 调用 Gitea API + import urllib.request + import urllib.parse + import json as json_module + + url = f"{GITEA_URL}/api/v1/repos/{GITEA_REPO}/pulls" + data = { + "title": title, + "head": branch, + "base": "main", + "body": body + } + + req = urllib.request.Request( + url, + data=json_module.dumps(data).encode('utf-8'), + headers={ + 'Authorization': f'token {GITEA_TOKEN}', + 'Content-Type': 'application/json' + }, + method='POST' + ) + + try: + with urllib.request.urlopen(req, timeout=30) as response: + result = json_module.loads(response.read().decode('utf-8')) + return { + "success": True, + "pr_number": result.get("number"), + "pr_url": result.get("html_url"), + "branch": branch + } + except urllib.error.HTTPError as e: + error_body = e.read().decode('utf-8') if e.fp else str(e) + return {"success": False, "error": f"API 错误: {e.code} - {error_body}"} + except Exception as e: + return {"success": False, "error": f"请求失败: {str(e)}"} + + +def get_status() -> dict: + """获取当前开发状态""" + result = { + "current_branch": None, + "spec_exists": False, + "planning_exists": False, + "uncommitted_changes": False + } + + # 当前分支 + branch_result = run_cmd('git branch --show-current') + if branch_result.returncode == 0: + result["current_branch"] = branch_result.stdout.strip() + + # spec 文件 + result["spec_exists"] = (PLANNING_DIR / 'spec.md').exists() + + # planning 目录 + result["planning_exists"] = PLANNING_DIR.exists() + + # 未提交的更改 + status_result = run_cmd('git status --porcelain') + result["uncommitted_changes"] = bool(status_result.stdout.strip()) + + return result + + +def main(): + parser = argparse.ArgumentParser(description='dev-workflow 辅助工具') + subparsers = parser.add_subparsers(dest='command', help='命令') + + # create-spec + spec_parser = subparsers.add_parser('create-spec', help='创建 spec 文件') + spec_parser.add_argument('--title', required=True, help='功能标题') + spec_parser.add_argument('--description', default='', help='功能描述') + + # create-branch + branch_parser = subparsers.add_parser('create-branch', help='创建功能分支') + branch_parser.add_argument('--spec', required=True, help='spec 文件路径') + + # create-pr + pr_parser = subparsers.add_parser('create-pr', help='创建 PR') + pr_parser.add_argument('--branch', required=True, help='分支名') + pr_parser.add_argument('--title', required=True, help='PR 标题') + pr_parser.add_argument('--body', default='', help='PR 描述') + + # status + subparsers.add_parser('status', help='查看状态') + + args = parser.parse_args() + + if args.command == 'create-spec': + result = create_spec(args.title, args.description) + elif args.command == 'create-branch': + result = create_branch(args.spec) + elif args.command == 'create-pr': + result = create_pr(args.branch, args.title, args.body) + elif args.command == 'status': + result = get_status() + else: + parser.print_help() + return + + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/dev-workflow/templates/spec-template.md b/dev-workflow/templates/spec-template.md new file mode 100644 index 0000000..239ac15 --- /dev/null +++ b/dev-workflow/templates/spec-template.md @@ -0,0 +1,78 @@ +# Spec: {{TITLE}} + +**创建时间:** {{DATE}} +**状态:** draft +**分支:** feature/{{BRANCH_NAME}} + +--- + +## 概述 + + + +## 背景 + + + +## 需求 + +### 功能需求 + +- [ ] **REQ-01**: +- [ ] **REQ-02**: +- [ ] **REQ-03**: + +### 非功能需求 + +- +- +- + +## 技术设计 + +### 实现方案 + + + +### 文件结构 + +``` +kb-skills/ +└── <技能目录>/ + ├── SKILL.md + └── <其他文件> +``` + +### 依赖 + +- + +## 测试计划 + +### 单元测试 + +- [ ] + +### 集成测试 + +- [ ] + +## 验收标准 + +1. +2. +3. + +## 风险与缓解 + +| 风险 | 缓解措施 | +|------|----------| +| | | + +--- + +## 变更日志 + +| 日期 | 变更 | +|------|------| +| {{DATE}} | 初始创建 | diff --git a/pr-review-gate/SKILL.md b/pr-review-gate/SKILL.md new file mode 100644 index 0000000..ad3169d --- /dev/null +++ b/pr-review-gate/SKILL.md @@ -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 通过 diff --git a/pr-review-gate/pr_gate.py b/pr-review-gate/pr_gate.py new file mode 100644 index 0000000..42d7538 --- /dev/null +++ b/pr-review-gate/pr_gate.py @@ -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() diff --git a/pr-review-gate/reviewers/logic.md b/pr-review-gate/reviewers/logic.md new file mode 100644 index 0000000..22ae818 --- /dev/null +++ b/pr-review-gate/reviewers/logic.md @@ -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**: 需求未满足,或存在严重的逻辑问题 diff --git a/pr-review-gate/reviewers/quality.md b/pr-review-gate/reviewers/quality.md new file mode 100644 index 0000000..51d31f9 --- /dev/null +++ b/pr-review-gate/reviewers/quality.md @@ -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**: 存在严重的质量问题(如缺少测试、代码无法理解) diff --git a/pr-review-gate/reviewers/security.md b/pr-review-gate/reviewers/security.md new file mode 100644 index 0000000..7acba9c --- /dev/null +++ b/pr-review-gate/reviewers/security.md @@ -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**: 存在中/高风险安全问题