This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/dev-workflow/dev_workflow.py
T
starmen1265 630dc09062 feat(dev-workflow): 添加 Cross-Debate 和 Cross-Review 模式
- 更新 SKILL.md:完整的 Cross 模式流程文档
- 更新 dev_workflow.py:新增 record-debate、record-review 命令
- 新增 debate-template.md:Cross-Debate 模板
- 用户介入点:Phase 1 方案选择 + Phase 6 最终审核
- 六条铁律:Briefing 原则、安全约束等
- 禁止 auto-merge:防止流程错误
2026-03-09 12:16:08 +08:00

454 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dev_workflow.py - Agent Engineering 开发流程辅助工具
用法:
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 record-debate --file .planning/debate.md
python3 dev_workflow.py record-review --round 1 --issues '[...]'
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'
# Cross 模式配置
CROSS_AGENTS = ['claude-code', 'codex', 'gemini']
DEBATE_FILE = PLANNING_DIR / 'debate.md'
REVIEW_FILE = PLANNING_DIR / 'review.json'
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, from_debate: bool = False) -> 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}}
**来源:** {{SOURCE}}
---
## 概述
{{DESCRIPTION}}
## 需求
### 功能需求
- [ ] **REQ-01**: 待补充
## 验收标准
1. 待补充
---
## Cross-Debate 汇总
{{DEBATE_SUMMARY}}
---
## 变更日志
| 日期 | 变更 |
|------|------|
| {{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)
content = content.replace('{{SOURCE}}', 'Cross-Debate' if from_debate else '用户需求')
# 如果有 debate 文件,提取汇总
if from_debate and DEBATE_FILE.exists():
debate_content = DEBATE_FILE.read_text(encoding='utf-8')
# 提取共识点部分
match = re.search(r'## 共识点\n(.*?)(?=\n## |$)', debate_content, re.DOTALL)
if match:
content = content.replace('{{DEBATE_SUMMARY}}', match.group(1).strip())
else:
content = content.replace('{{DEBATE_SUMMARY}}', '见 .planning/debate.md')
else:
content = content.replace('{{DEBATE_SUMMARY}}', '无')
# 写入文件
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,
"from_debate": from_debate
}
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(禁止 auto-merge)"""
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 ""
# 添加 Cross 模式说明
review_info = ""
if REVIEW_FILE.exists():
try:
review_data = json.loads(REVIEW_FILE.read_text())
total_issues = sum(len(v) for v in review_data.get('rounds', {}).values())
rounds = len(review_data.get('rounds', {}))
review_info = f"\n\n**Cross-Review:** {rounds} 轮审核,修复 {total_issues} 个问题"
except:
pass
body += review_info
body += "\n\n⚠️ **禁止 auto-merge** - 等待人工最终审核"
# 调用 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,
"warning": "禁止设置 auto-merge"
}
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 record_debate(file: str = None) -> dict:
"""记录 Cross-Debate 结果"""
PLANNING_DIR.mkdir(parents=True, exist_ok=True)
debate_content = None
if file:
file_path = Path(file)
if file_path.exists():
debate_content = file_path.read_text(encoding='utf-8')
if debate_content:
DEBATE_FILE.write_text(debate_content, encoding='utf-8')
return {
"success": True,
"file": str(DEBATE_FILE),
"message": "Cross-Debate 结果已记录"
}
else:
# 创建模板
template = f"""# Cross-Debate 结果
**创建时间:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
**需求:** <!-- 填写需求 -->
---
## Agent 方案
### Claude Code(保守派)
**方案:**
<!-- 填写 -->
**理由:**
<!-- 填写 -->
---
### Codex(全面派)
**方案:**
<!-- 填写 -->
**理由:**
<!-- 填写 -->
---
### Gemini(快速派)
**方案:**
<!-- 填写 -->
**理由:**
<!-- 填写 -->
---
## 共识点
- ✅ <!-- 共识1 -->
- ✅ <!-- 共识2 -->
## 分歧点
| Agent | 方案 | 理由 |
|-------|------|------|
| <!-- Agent --> | <!-- 方案 --> | <!-- 理由 --> |
## 用户决策
**选择方案:** <!-- A/B/C 或调整 -->
---
"""
DEBATE_FILE.write_text(template, encoding='utf-8')
return {
"success": True,
"file": str(DEBATE_FILE),
"message": "Cross-Debate 模板已创建,请填写"
}
def record_review(round_num: int, issues: str = None) -> dict:
"""记录 Cross-Review 结果"""
PLANNING_DIR.mkdir(parents=True, exist_ok=True)
# 读取现有数据
review_data = {"rounds": {}}
if REVIEW_FILE.exists():
try:
review_data = json.loads(REVIEW_FILE.read_text())
except:
pass
# 解析 issues
issues_list = []
if issues:
try:
issues_list = json.loads(issues)
except:
# 简单格式:每行一个问题
issues_list = [{"level": "medium", "desc": line} for line in issues.split('\n') if line.strip()]
# 添加本轮结果
round_key = f"round_{round_num}"
review_data["rounds"][round_key] = {
"timestamp": datetime.now().isoformat(),
"issues": issues_list
}
# 统计
review_data["stats"] = {
"total_rounds": len(review_data["rounds"]),
"total_issues": sum(len(v.get("issues", [])) for v in review_data["rounds"].values())
}
REVIEW_FILE.write_text(json.dumps(review_data, ensure_ascii=False, indent=2), encoding='utf-8')
return {
"success": True,
"file": str(REVIEW_FILE),
"round": round_num,
"issues_count": len(issues_list),
"stats": review_data["stats"]
}
def get_status() -> dict:
"""获取当前开发状态"""
result = {
"current_branch": None,
"spec_exists": False,
"debate_exists": False,
"review_exists": False,
"planning_exists": False,
"uncommitted_changes": False,
"cross_mode": False
}
# 当前分支
branch_result = run_cmd('git branch --show-current')
if branch_result.returncode == 0:
result["current_branch"] = branch_result.stdout.strip()
# 各种文件状态
result["spec_exists"] = (PLANNING_DIR / 'spec.md').exists()
result["debate_exists"] = DEBATE_FILE.exists()
result["review_exists"] = REVIEW_FILE.exists()
result["planning_exists"] = PLANNING_DIR.exists()
result["cross_mode"] = result["debate_exists"] or result["review_exists"]
# 未提交的更改
status_result = run_cmd('git status --porcelain')
result["uncommitted_changes"] = bool(status_result.stdout.strip())
# 如果有 review 数据,显示统计
if REVIEW_FILE.exists():
try:
review_data = json.loads(REVIEW_FILE.read_text())
result["review_stats"] = review_data.get("stats", {})
except:
pass
return result
def main():
parser = argparse.ArgumentParser(description='dev-workflow 辅助工具(Cross 模式)')
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='功能描述')
spec_parser.add_argument('--from-debate', action='store_true', help='从 Cross-Debate 结果生成')
# 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(禁止 auto-merge)')
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 描述')
# record-debate
debate_parser = subparsers.add_parser('record-debate', help='记录 Cross-Debate 结果')
debate_parser.add_argument('--file', help='输入文件路径(可选)')
# record-review
review_parser = subparsers.add_parser('record-review', help='记录 Cross-Review 结果')
review_parser.add_argument('--round', type=int, required=True, help='审核轮次')
review_parser.add_argument('--issues', help='问题列表(JSON 或简单文本)')
# status
subparsers.add_parser('status', help='查看状态')
args = parser.parse_args()
if args.command == 'create-spec':
result = create_spec(args.title, args.description, args.from_debate)
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 == 'record-debate':
result = record_debate(args.file)
elif args.command == 'record-review':
result = record_review(args.round, args.issues)
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()