git-pr-review
当用户明确要求“review 某个 GitHub PR”“评估某个 pull request 是否值得 merge”“帮我判断这个 PR 怎么处理”时使用。基于用户提供的 GitHub 仓库地址、PR 编号/链接和补充说明,进行只读、证据驱动的 PR 审查:理解 PR 解决的问题、评估方案优劣与局限、默认优先使用内置“好 PR”标准并在必要时联网补充、识别恶意或高风险改动,并输出是否建议 merge 的 Markdown 决策报告。⚠️ 不适用:用户要你直接修改 PR 代码、直接 merge PR、或在本地执行 PR 分支中的不可信代码。
Install
npx skills add https://github.com/huangwb8/skills/tree/main/skills/alpha/git-pr-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install huangwb8-skills@llmmart
git clone https://github.com/huangwb8/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole huangwb8/skills collection as a plugin from our marketplace. Git is the plain clone.
README
git-pr-review
本 README 面向使用者:如何触发并正确使用 git-pr-review skill。
执行规范在 SKILL.md;默认参数与命名规则在 config.yaml。
用法
推荐用法:
请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并判断是否值得 merge。
输入:仓库地址 `https://github.com/owner/repo`,PR `https://github.com/owner/repo/pull/123`
输出:项目根目录下 1 份 Markdown 审查报告
进阶用法:
请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并重点检查是否存在恶意代码或供应链风险。
输入:仓库地址 `https://github.com/owner/repo`,PR `#123`,另外参考我附上的背景说明
输出:项目根目录下 1 份 Markdown 审查报告;所有中间文件放到 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/`
并行独立评审版:
请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并做 5 次独立评审后再综合结论。
输入:仓库地址 `https://github.com/owner/repo`,PR `#123`
输出:项目根目录下 1 份最终 Markdown 审查报告;`.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` 里保留并行独立评审的全部中间产物
它能帮你做什么
git-pr-review 适合下面这些场景:
- 你想知道某个 PR 到底解决了什么问题
- 你不确定这个 PR 的方案值不值得 merge
- 你担心 PR 里藏着恶意逻辑、安全问题、权限扩大或 CI/CD 风险
- 你担心 PR 引入新的依赖、第三方代码或资源后会带来 license 冲突
- 你希望把这个 PR 和社区对“好 PR”的标准做一次对照
- 你希望让多个独立 reviewer 视角并行审查,再综合得出更稳的判断
- 你想把结论沉淀成一份可转发、可归档的 Markdown 报告
工作方式
这个 skill 默认是只读评审:
- 会读取 GitHub 上的 PR 信息、diff、评论、CI 状态和关联 issue
- 默认优先使用 skill 内置的“好 PR”标准参考,而不是每次实时联网
- 只有用户明确要求最新口径、指定特定社区规范,或内置标准明显不足时,才会联网补充“好 PR”标准来源
- 会默认基于
parallel-vibe做 5 次独立评审 - 会在涉及依赖、vendored 代码、复制资源时给出 license / 合规建议
- 会把所有中间文件放进工作目录下的
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/ - 会在项目根目录生成最终报告
它不会:
- 修改仓库代码
- 自动 merge PR
- approve PR
- checkout PR 分支后运行不可信代码
输出文件
默认会生成:
Git-PR-Review_{repo}_{pr}_{yyyy-mm-dd-hh-mm}.md
例如:
Git-PR-Review_openai_openai-python_pr-2451_2026-03-24-15-30.md
中间文件默认位于:
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/
其中通常包含:
manifest.json:本次评审的路径和元数据raw/README.md:原始材料目录说明notes/user_context.md:用户补充背景notes/community_good_pr.md:社区标准摘记references/good-pr-standards.md:skill 内置的“好 PR”标准参考notes/license_review.md:license / 合规审查笔记(如适用)evidence/key_findings.md:关键发现evidence/missing_items.md:缺失证据与影响parallel_review/parallel_plan.json:并行独立评审计划parallel_review/parallel_runs/.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/parallel-vibe/<project_id>/...:独立 reviewer threads 产物parallel_review/independent_review_summary.md:独立评审聚合结果
关键行为
Fail-fast 校验
脚本会尽早拦截明显错误,避免你在错误上下文上浪费时间:
- 仓库地址不是 GitHub 仓库,或误传成
issues/、pull/、tree/这类子页面 URL - PR URL 不是 GitHub Pull Request,或编号格式不是
#123/123/pr-123 - 仓库与 PR URL 不属于同一个仓库
- 最终报告文件名或章节结构不符合约定
工作区隔离
- 默认中间文件只允许写入
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/ - 最终 Markdown 报告默认写到当前工作目录
- 如果你明确指定其他
workspace_dir或report_dir,skill 会按你的指定执行
默认独立评审次数
- 默认会做
5次独立评审 - 如果你明确指定
review_count=7或“做 7 次独立评审”,skill 会按你的值执行 - 在底层辅助脚本里,这个值会映射为
build_parallel_review_plan.py --n <review_count> - 如果你只想单次评审,也可以明确要求
review_count=1
使用示例
示例 1:常规技术评审
请使用 git-pr-review skill 帮我 review 这个 PR。
输入:仓库 `https://github.com/pallets/flask`,PR `https://github.com/pallets/flask/pull/5432`
输出:给我一份是否建议 merge 的 Markdown 报告
示例 2:重点怀疑恶意行为
请使用 git-pr-review skill 评估这个 PR 是否恶意。
输入:仓库 `https://github.com/owner/repo`,PR `#87`
输出:一份带风险等级和处理建议的报告
示例 3:带团队背景说明
请使用 git-pr-review skill 审查这个 PR。
输入:仓库 `https://github.com/owner/repo`,PR `#201`
另外,还有下列参数约束:
- 重点看 CI/CD、权限配置和 secrets 是否被动了手脚
- 我们团队更偏好小而清晰、易回滚的 PR
- 输出时请明确写出“是否建议 merge”
示例 4:指定独立评审次数
请使用 git-pr-review skill 审查这个 PR,并做 7 次独立评审。
输入:仓库 `https://github.com/owner/repo`,PR `#88`
输出:最终审查报告中请单独总结 7 次独立评审的共识与分歧
结果里会回答哪些问题
最终报告通常会明确回答:
- 多个独立 reviewer 的 recommendation / risk 是否形成共识
- 这个 PR 解决的问题是否真实、是否描述清楚
- 方案的优势、局限和潜在替代路径
- 改动范围是否合理,是否便于 review 和回滚
- 是否存在恶意或高风险信号
- 是否存在 license 冲突、copyleft 风险、第三方归属缺失或需要法务确认的点
- 它是否符合社区里“好 PR”的基本标准
- 如果证据不够,缺的是什么、会如何影响结论
- 最后到底建议
Merge、Request changes还是Do not merge
常见问题
Q:它会不会自己把 PR merge 掉?
不会。除非你明确提出,否则这个 skill 不会执行 merge、approve、rebase、squash 这类操作。
Q:它会不会运行 PR 里的代码?
默认不会。这个 skill 的原则是只读评审,避免执行不可信代码。
Q:如果我想把中间文件放到别处呢?
可以。默认目录是 .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/,但如果你明确指定其他目录,skill 会按你的目录来。
Q:它会只看 diff 吗?
不会。它还会结合 PR 描述、评论、CI 状态、关联 issue,以及外部社区对“好 PR”的标准一起判断。
Q:它会检查 license 问题吗?
会。尤其当 PR 引入新依赖、复制第三方代码、字体、图标、模板、数据或模型资源时,它会明确给出 license / 合规风险和建议动作。
Q:默认为什么是 5 次独立评审?
因为 5 个独立视角通常足够覆盖:整体质量、安全风险、可维护性、测试/验证、社区标准这几类核心问题,同时又不会把成本拉得过高。
备选用法(脚本/硬编码流程)
步骤 1:创建隔离工作区
python3 git-pr-review/scripts/prepare_review_job.py \
--repo "https://github.com/owner/repo" \
--pr "https://github.com/owner/repo/pull/123"
步骤 2:完成评审后校验产物
python3 git-pr-review/scripts/validate_review_artifacts.py \
--manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \
--report /absolute/path/to/Git-PR-Review_<...>.md
步骤 3:生成 parallel-vibe 并行独立评审计划
# 例如用户要求 review_count=5 时,这里传 --n 5
python3 git-pr-review/scripts/build_parallel_review_plan.py \
--manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \
--n 5
步骤 4:运行 parallel-vibe 独立评审
# 推荐直接复制 `parallel_review_job.json` 里的 `recommended_command`
python3 ../parallel-vibe/scripts/parallel_vibe.py \
--plan-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_plan.json \
--src-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/input_snapshot \
--out-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_runs \
--project-id <parallel_review_job.json.project_id>
补充说明:
build_parallel_review_plan.py会把解析后的parallel-vibe脚本路径、固定project_id和可直接执行的recommended_command写进parallel_review/parallel_review_job.json- 如果你补充了
raw/、notes/、evidence/里的材料,请重新运行build_parallel_review_plan.py,让输入快照和project_id一起刷新
步骤 5:聚合独立评审结果
python3 git-pr-review/scripts/aggregate_parallel_reviews.py \
--job-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_review_job.json
如果 parallel-vibe 尚未执行完成,或某个 thread 没有生成 RESULT.md,聚合脚本会直接报错并提示先补齐输入,避免拿不完整结果做 merge 决策。
更多文档
SKILL.md:执行流程与硬规则config.yaml:版本、命名与审查口径references/report-template.md:报告模板references/good-pr-standards.md:内置“好 PR”标准参考references/security-checklist.md:恶意/安全审查清单references/license-checklist.md:license / 合规审查清单references/community-research-playbook.md:社区标准检索建议references/parallel-review-result-template.md:独立评审 thread 模板references/parallel-vibe-integration.md:parallel-vibe 集成说明
WHICHMODEL - 模型选择最佳实践
最后更新:2026-03-24
披露信息
- 覆盖厂商:Anthropic、OpenAI
- 来源类型:官方模型文档 / 官方模型总览
- 局限性:本节优先采用官方资料来保证时效与权威性,未纳入社区主观体验对比;因此更适合做“保守默认选择”,不适合做极端成本优化结论
场景 1:默认 PR 审查
| 项目 | 建议 |
|---|---|
| 推荐模型 | Claude Sonnet 4 / GPT-5.2 |
| 推荐原因 | 这个 skill 需要同时处理 diff、评论、社区资料和安全判断,属于“中高复杂度的多源证据综合”;默认优先选择推理和速度更平衡的模型 |
| 适用情况 | 常规 PR、几十到上百行 diff、需要联网和结构化报告输出 |
场景 2:高风险 / 安全敏感 PR
| 项目 | 建议 |
|---|---|
| 推荐模型 | Claude Opus 4.1 / GPT-5.2 |
| 推荐原因 | 当 PR 涉及 CI/CD、权限、密钥、供应链或大规模重构时,更强的上下文整合和细粒度推理更重要 |
| 适用情况 | 可疑 PR、超大 diff、需要更谨慎的恶意行为识别 |
场景 3:快速分诊
| 项目 | 建议 |
|---|---|
| 推荐模型 | Claude Haiku 3.5 / GPT-5 mini |
| 推荐原因 | 仅做“先看值不值得深审”的低成本分诊时,可以用更便宜更快的模型先产出初筛结论 |
| 适用情况 | 批量 triage、多 PR 排队、先判断是否需要人工深审 |
选择原则
- 只做初筛时优先“快而便宜”;一旦涉及安全与 merge 决策,就升级到默认档或高风险档。
- 这个 skill 的核心不是写代码,而是跨来源证据综合判断,所以默认更适合通用高推理模型,而不是纯编码特化模型。
- 如果你打算把这个 skill 嵌入会频繁读取本地仓库、做更多 agentic coding 操作的工作流里,再考虑编码特化模型;单次 PR 决策报告通常不需要这样做。
官方参考
- Anthropic Models Overview: https://docs.anthropic.com/en/docs/about-claude/models/overview
- OpenAI Models Overview: https://platform.openai.com/docs/models
Skill manifest
Git PR Review
目标
当用户明确要求“review 某个 GitHub PR”“评估某个 pull request 是否值得 merge”“帮我判断这个 PR 怎么处理”时使用。基于用户提供的 GitHub 仓库地址、PR 编号/链接和补充说明,进行只读、证据驱动的 PR 审查:理解 PR 解决的问题、评估方案优劣与局限、默认优先使用内置“好 PR”标准并在必要时联网补充、识别恶意或高风险改动,并输出是否建议 merge 的 Markdown 决策报告。⚠️ 不适用:用户要你直接修改 PR 代码、直接 merge PR、或在本地执行 PR 分支中的不可信代码。
流程
输入
你需要确认的输入
github_repo(必需)- GitHub 仓库根地址或
owner/repo,例如https://github.com/owner/repo - 不接受
issues/、pull/、tree/这类子页面 URL
- GitHub 仓库根地址或
github_pr(必需)- PR URL、
#123、123或pr-123
- PR URL、
extra_instructions(可选)- 用户已有判断、关注点、禁区、参考材料、团队背景
review_count(可选)- 独立评审次数,默认 5;用户明确指定时以用户要求为准
- 当你调用
build_parallel_review_plan.py时,把它映射为--n <review_count>
workspace_dir(可选)- 默认
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/
- 默认
report_dir(可选)- 默认当前工作目录(项目根)
执行步骤
核心原则
- 只读优先:默认只能读取 GitHub 页面、API 返回、diff、评论、CI 状态、相关 issue/文档;不要修改源代码。
- 绝不主动 merge:除非用户明确要求,否则不要执行 merge、rebase、squash、approve、request review 等操作。
- 绝不执行不可信 PR 代码:不要
gh pr checkout、不要运行 PR 分支脚本、不要安装 PR 引入的依赖、不要触发可疑 CI/CD。 - 中间文件隔离:所有中间文件必须保存在工作目录下的隐藏目录
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/;若用户另有指定,才使用用户指定目录。 - 证据驱动:所有结论都要回到证据,明确引用 diff、评论、CI、issue、社区资料或官方文档。
- 合规不忽略:如果 PR 触及依赖、vendored 代码、复制粘贴第三方内容、资源资产或许可证文件,必须显式审查 license 风险与兼容性。
标准工作流
1. 初始化隔离工作区
优先使用确定性脚本创建本次评审目录与建议输出文件名:
python3 git-pr-review/scripts/prepare_review_job.py \
--repo "https://github.com/owner/repo" \
--pr "https://github.com/owner/repo/pull/123"
脚本会:
- 校验仓库地址与 PR URL 是否属于同一 GitHub 仓库
- 创建本次运行目录(默认
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/) - 生成
manifest.json - 预创建最小占位文件:
raw/README.mdnotes/user_context.mdnotes/community_good_pr.mdevidence/key_findings.mdevidence/missing_items.md
- 输出建议的最终报告路径(默认项目根)
从这一步开始,所有抓取到的原始材料、分析笔记、临时摘要、命令输出都写入该工作区。
2. 只读获取 PR 证据
优先选择不会修改仓库状态的方式读取 PR:
- GitHub 网页 / API /
.patch/.diff gh pr view、gh pr diff、gh api这类只读命令- 仓库内公开 issue、discussion、相关文档、CI 状态页
建议至少保存下列材料到工作区:
raw/pr_meta.*:标题、作者、状态、标签、基线分支、merge 状态、CI 状态raw/pr_diff.*:完整 diff 或 patchraw/pr_comments.*:review comments / discussion(如有)raw/linked_issues.*:关联 issue / discussion / docs(如有)notes/user_context.md:用户给的补充信息notes/community_good_pr.md:关于“好 PR”的社区标准摘记与链接notes/license_review.md:license / 合规审查笔记(如本次 PR 相关)evidence/missing_items.md:缺失材料、原因与影响
如果某项拿不到,要在工作区里记录“未获取原因”以及它对结论的影响,而不是静默跳过。
3. 基于 parallel-vibe 做 N 次独立评审(默认 5 次)
目标:
- 使用
parallel-vibe在多个独立 workspace 中做 N 次彼此独立的 PR 审查 - 默认
N=5 - 每个 thread 必须独立产出
RESULT.md - 最后把多份
RESULT.md聚合为一份统一摘要,再用于最终报告
推荐顺序:
python3 git-pr-review/scripts/build_parallel_review_plan.py \
--manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \
--n 5
这里的 --n 是 helper script 参数;如果用户说“做 7 次独立评审”,等价于 review_count=7,最终在脚本层落成 --n 7。
该脚本会在本次 run 目录下生成:
parallel_review/input_snapshot/:供每个 thread 复制的审查输入快照parallel_review/parallel_plan.json:parallel-vibe的计划文件parallel_review/parallel_review_job.json:后续运行与聚合所需路径、已解析好的parallel-vibe脚本路径、固定的project_id与recommended_command
重要说明:
- 优先直接执行
parallel_review/parallel_review_job.json里的recommended_command,避免手动拼路径或project_id - 如果
raw/、notes/、evidence/有变化,必须重新运行build_parallel_review_plan.py,让输入快照和project_id一起刷新
然后运行:
# 更推荐直接复制 `parallel_review_job.json` 里的 `recommended_command`
python3 ../parallel-vibe/scripts/parallel_vibe.py \
--plan-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_plan.json \
--src-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/input_snapshot \
--out-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_runs \
--project-id <parallel_review_job.json.project_id>
并行独立评审完成后,聚合结果:
python3 git-pr-review/scripts/aggregate_parallel_reviews.py \
--job-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_review_job.json
如果 parallel-vibe 还没跑完、某个 thread 缺少 RESULT.md,或聚合输入不完整,聚合脚本应明确报错并停止,而不是静默生成误导性摘要。
聚合脚本会生成:
parallel_review/independent_review_summary.mdparallel_review/independent_review_summary.json
要求:
- 每个 thread 都要基于当前 workspace 中的材料独立审查,不看其他 thread 结果
- 每个 thread 的
RESULT.md必须至少包含 recommendation / risk / evidence gaps - 最终主报告必须明确综合独立评审的共识与分歧
4. 理解这个 PR 在解决什么
至少回答清楚:
- PR 试图解决的核心问题是什么
- 这个问题是否真实存在,证据来自哪里
- PR 采用了什么方案
- 方案的主要优点、局限和潜在替代方案
- PR 是否与标题、描述、commit、测试、文档保持一致
如果 PR 描述模糊,必须从 diff、关联 issue、评论线程中补足上下文。
5. 恶意 / 高风险 PR 审查
必须显式判断该 PR 是否存在恶意或高风险特征,并给出风险等级:
Low / Medium / High / Critical
重点检查:
- 是否存在数据窃取、凭证泄露、遥测偷报、后门逻辑
- 是否引入可疑下载、远程执行、shell 注入、SQL 注入、权限提升、破坏性删除
- 是否修改 CI/CD、部署脚本、权限配置、密钥读取路径、发布流程
- 是否引入难以解释的混淆代码、base64/hex 载荷、动态执行链
- 是否借“重构/清理”名义绕开测试、放宽校验、关闭安全保护
- 是否故意减少日志、删除告警、掩盖审计痕迹
- 是否存在明显不符合项目目标的改动范围漂移
只要怀疑恶意,就要把建议提升为:
不要 merge建议人工安全复核 / 维护者升级处理
6. License / 合规审查
这是强制步骤之一。即使用户没有主动提到 license,只要 PR 涉及下列内容,就必须显式检查:
- 新依赖、依赖升级、包管理器锁文件
- vendored 代码、复制的第三方脚本/模板/字体/图标/数据
LICENSE/NOTICE/ copyright header- 模型权重、数据集、示例代码、前端资源
至少回答清楚:
- PR 是否引入了新的 license 风险或兼容性风险
- 是否存在互相冲突的 license / header / 声明
- 是否需要更新
LICENSE、NOTICE、README、第三方声明文档 - 该风险是否需要法务/维护者额外确认
对于 license 不明确、明显冲突、或可能触发 GPL/AGPL/SSPL 等传播义务的情况:
- 不能直接给出乐观 merge 建议
- 应提升为
Request changes、Do not merge或至少Escalate security review/ 合规复核
参考:references/license-checklist.md
7. 使用内置“好 PR”标准(默认不实时联网)
这里的默认做法不是每次执行都实时联网。
本 skill 已经把“什么是好 PR”的基础标准沉淀到:
references/good-pr-standards.md
并且在初始化工作区时,会自动把该参考摘要写入:
notes/community_good_pr.md
默认执行时:
- 优先使用
notes/community_good_pr.md里的内置标准 - 结合当前 PR 做对照分析
- 在最终报告中明确写出“哪些维度符合 / 不符合”
只有在以下情况才需要再联网补充:
- 用户明确要求查看最新社区口径
- 用户指定某个特定社区 / 组织 / 仓库的 PR 规范
- 当前内置标准不足以覆盖该 PR 的特殊场景
即使需要补充联网,也应把新增来源继续沉淀到本次 run 的 notes/community_good_pr.md,而不是只在脑中使用。
不是机械照抄“最佳实践”,而是要回答:
- 这个 PR 在哪些维度上符合好 PR 的定义
- 哪些维度不符合
- 不符合之处是“小瑕疵”还是“阻断 merge 的问题”
8. 输出决策报告
将最终结论写成 Markdown,默认放在项目根目录,文件名格式:
Git-PR-Review_{repo_slug}_{pr_slug}_{timestamp}.md
例如:
Git-PR-Review_openai_openai-python_pr-2451_20260324153022.md
报告必须包含以下章节:
## 结论摘要## 独立评审综合结果## PR 在解决什么问题## 方案分析## 恶意/安全风险审查## License / 合规审查## 与“好 PR”社区标准的对照## 关键证据## 证据不足与待确认点## 建议的处理方式
完整模板不要直接内嵌在 SKILL.md,而是统一引用:
references/report-template.md
写最终报告时,必须综合:
- 原始证据(
raw/、notes/、evidence/) parallel_review/independent_review_summary.md- 如有必要,
parallel_review/parallel_runs/.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/parallel-vibe/<project_id>/@main/summary.md
9. 完成前自检
在交付前运行校验脚本,确认:
- 工作区结构完整
- 默认模式下工作区仍位于隐藏目录
- 最终报告文件名合规
- 最终报告包含配置要求的所有必需章节
- 没有把中间产物散落到工作区外
python3 git-pr-review/scripts/validate_review_artifacts.py \
--manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \
--report /abs/path/to/Git-PR-Review_<...>.md
可读参考
references/report-template.md:最终报告模板references/security-checklist.md:恶意/高风险 PR 审查清单references/license-checklist.md:license / 合规审查清单references/community-research-playbook.md:如何搜索“好 PR”社区标准references/parallel-review-result-template.md:独立评审 thread 的RESULT.md结构references/parallel-vibe-integration.md:parallel-vibe集成方式与关键产物
输出
输出要求
- 最终交付物:1 份 Markdown 报告
- 报告用语要明确,不要模棱两可
- 结论必须给出处理建议,不要只给“分析但不表态”
- 如果证据不足,要明确说明不足项,以及这如何影响你的建议
输出管理
BenszAPI 任务工作区
校验
交付前运行 validate_review_artifacts.py,确认 manifest、原始证据、独立评审汇总和最终报告齐全,报告包含配置要求的全部章节、建议明确且所有结论可回到证据;核对工作区仍在 .bensz-api 隐藏目录且未执行 PR 代码或远程写操作。
失败与恢复
仓库/PR 证据获取失败、并行 thread 缺少 RESULT.md、聚合输入不完整或报告校验失败时,保留已获取材料与错误摘要,停止生成乐观结论并明确证据缺口;不得通过 checkout、运行不可信代码或修改远程状态来绕过阻塞。
约束
公共硬约束
本块由 docs/templates/skill-common-constraints.md 统一维护;每个 SKILL.md 的 ## 约束 必须逐字同步本块,不得在副本中改写公共规则。
- 任务需要落盘时,使用唯一的
./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/根目录;共享材料放入shared/,Skill 专属材料放入该 Skill 的input/、output/、log/。 - 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。
- 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。
- 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。
- 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。
- Skill 版本唯一记录在自身
config.yaml:skill_info.version;公开 API、协议、目录或配置变更同步文档与CHANGELOG.md。 bensz-collect-bugs是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入~/.bensz-skills/bugs/,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。
Skill 专属约束
明确禁止事项
- 不要修改目标仓库代码
- 不要 merge PR
- 不要 approve PR
- 不要 checkout PR 分支并执行代码
- 不要把 API token、cookie、认证信息写入工作区
- 不要把中间文件写到
.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/之外(除最终 Markdown 报告)
Files (skills)
-
references
-
community-research-playbook.md 909 B
# “好 PR” 社区标准检索建议 目标:不是泛泛搜索“如何做代码审查”,而是提炼“什么样的 PR 才算高质量、值得 merge”。 建议检索方向: - GitHub pull request best practices - what makes a good pull request maintainer - good PR size description tests reviewability - GitHub pull request guidelines maintainers - open source maintainer pull request quality 优先提炼的维度: - 粒度是否合适(小而聚焦,还是范围漂移) - 描述是否清楚(背景、动机、影响范围) - 是否有测试/验证/回滚考虑 - 是否方便 reviewer 理解与审阅 - 是否与 issue / 文档 / CI 状态形成闭环 输出到最终报告时,尽量写成: - 来源是什么 - 来源链接是什么 - 发布日期或访问日期是什么 - 它提出了什么标准 - 当前 PR 在该标准下表现如何 - 这会不会影响 merge 决策 -
good-pr-standards.md 4 KB
# Good PR Standards 最后整理时间:2026-03-24 这份文档是 `git-pr-review` 内置的“好 PR”标准参考。 默认执行时优先使用本文件,不要求每次实时联网。 ## 来源 1. GitHub Docs: Setting guidelines for repository contributors 链接:https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors 访问日期:2026-03-24 2. GitHub Docs: About issue and pull request templates 链接:https://docs.github.com/enterprise-cloud@latest/communities/using-templates-to-encourage-useful-issues-and-pull-requests/manually-creating-a-single-issue-template-for-your-repository 访问日期:2026-03-24 3. GitHub Docs: About code owners 链接:https://docs.github.com/articles/about-code-owners 访问日期:2026-03-24 4. GitHub Docs: About pull request reviews 链接:https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews 访问日期:2026-03-24 5. Balachandran, Sawant et al. What Makes a Code Change Easier to Review 链接:https://www.mozillafoundation.org/en/research/library/what-makes-a-code-change-easier-to-review-an-empirical-investigation-on-code-change-reviewability/ 访问日期:2026-03-24 ## 沉淀出的核心标准 ### 1. 目标清楚 好 PR 应该让 reviewer 很快知道: - 它要解决什么问题 - 为什么现在要改 - 改动影响什么范围 如果 reviewer 需要从大量 diff 猜意图,这通常不是好信号。 ### 2. 描述充分且结构化 好 PR 通常会提供: - 背景 / 动机 - 主要改动点 - 测试或验证方式 - 风险、限制或待确认点 GitHub 官方关于贡献指南和 PR 模板的实践,本质上都在推动这件事:让贡献者提交“格式良好、信息完整”的 PR。 ### 3. 粒度合适、便于 review 研究和工程实践都支持一个结论: - 更小、更聚焦、边界清楚的改动,通常更容易 review - 把多个不相关改动揉在一起,会降低 review 质量 - tangled change(缠结改动)会让理解、回滚和追责都变难 所以评审时要看: - 这个 PR 是单一问题还是混了多个目标 - 是否存在 scope drift(标题说 A,diff 里还夹了 B/C) ### 4. 可验证 好 PR 不只是“看起来有道理”,还要便于验证。 常见正向信号: - 有测试 - 有复现步骤或验证步骤 - CI 信号清楚 - 对 reviewer 来说可重复检查 如果缺少测试、CI 失败、验证方式含糊,通常至少应降低 merge 信心。 ### 5. 容易找到合适 reviewer GitHub 官方关于 code owners 和 review request 的机制说明了一个事实: - 好 PR 应该能较容易路由到正确 reviewer - 改动范围清晰时,CODEOWNERS / reviewer assignment 才真正有效 如果改动横跨太多模块、责任边界模糊,就会降低 review 质量和 merge 可靠性。 ### 6. review 反馈可阻断 merge 时,要严肃对待 GitHub 官方 review 机制强调: - `Request changes` 不是装饰性意见 - 在启用了 required reviews 的仓库中,这类反馈本身就意味着 merge gate 因此在 `git-pr-review` 里: - 如果 PR 存在阻断性问题,不应把它描述成“小瑕疵” - 要明确区分“可 merge 但建议后补”与“当前不应 merge” ### 7. 需要显式说明风险与限制 好 PR 不应只讲优点。 更可靠的 PR 往往会主动说明: - 已知限制 - 未覆盖场景 - 迁移 / 回滚风险 - 还需要谁确认 如果 PR 完全不提这些,而 diff 又涉及高风险路径,review 时要主动补这一层判断。 ## 在 git-pr-review 里的使用方式 最终报告至少要回答: - 目标是否清楚 - 描述是否充分 - 粒度是否合适 - 是否可验证 - reviewer / ownership 是否清晰 - 是否存在阻断 merge 的问题 - 风险和限制是否被诚实披露 ## 使用提醒 这份参考是“默认基线”,不是死板 checklist。 如用户明确要求最新社区口径、特定组织规范、或当前 PR 场景明显超出这份基线,再联网补充即可。 -
license-checklist.md 1.8 KB
# License / 合规审查清单 当 PR 涉及新依赖、vendored 代码、复制第三方代码、模板、字体、图标、数据集、模型权重、示例代码或许可证文件时,必须做 license 审查。 优先检查: 1. 新增依赖或升级依赖 - 是否引入新的第三方包、库、插件、模型或前端资源 - 其 license 是什么 - 新 license 是否与仓库现有 license 或分发方式冲突 2. 复制/粘贴或 vendored 代码 - 是否把外部仓库的代码、脚本、头文件、模板、字体、图标、数据直接拷入仓库 - 是否保留原作者声明、copyright、LICENSE 文件、NOTICE 文件 - 是否需要额外署名或保留许可证文本 3. copyleft / 强传染性条款 - 是否引入 GPL、AGPL、LGPL、SSPL 或其他可能影响整体分发义务的内容 - 该项目当前分发方式是否允许这种义务存在 - 如果是服务端部署场景,AGPL/SSPL 风险是否会触发 4. 专有或限制性条款 - 是否引入禁止商用、禁止再分发、禁止修改、要求付费授权或仅限内部使用的内容 - PR 描述是否掩盖了这些限制 5. 许可证不一致或缺失 - 新增文件是否带有互相冲突的 header/license notice - 新依赖或复制来的代码是否根本没有明确 license - 如果 license 不明确,默认不能视为可安全 merge 6. 仓库级影响 - 当前仓库已有主许可证是什么 - 该 PR 是否需要同时更新 `LICENSE`、`NOTICE`、`README`、依赖清单、第三方声明文档 建议输出: - 结论:无明显 license 风险 / 存在待确认风险 / 明显冲突,不建议 merge - 风险来源:依赖 / vendored 代码 / 文件头 / 资源资产 / 文档缺失 - 建议动作:补许可证声明、补第三方归属、法务确认、替换依赖、不要 merge -
parallel-review-result-template.md 527 B
# Independent Parallel Review RESULT Template ```markdown # Independent PR Review ## Final Call - Recommendation: Merge / Merge after fixes / Request changes / Do not merge / Escalate security review - Risk Level: Low / Medium / High / Critical - Confidence: High / Medium / Low ## Problem Understanding - ... ## Strengths - ... ## Limitations - ... ## Security Review - ... ## License Review - ... ## Good PR Criteria Comparison - ... ## Key Evidence - ... ## Evidence Gaps - ... ## Suggested Disposition - ... ``` -
parallel-vibe-integration.md 1.3 KB
# parallel-vibe 集成说明 `git-pr-review` 的并行独立评审默认通过 `parallel-vibe` 落地。 推荐顺序: 1. `prepare_review_job.py` 2. `build_parallel_review_plan.py` 3. 优先执行 `parallel_review_job.json` 里的 `recommended_command`;若手动执行,则使用 `../parallel-vibe/scripts/parallel_vibe.py --plan-file ... --src-dir ... --out-dir ... --project-id ...` 4. `aggregate_parallel_reviews.py` 5. 宿主 AI 综合 `independent_review_summary.md` 与原始证据写最终报告 默认并行独立评审次数:5。 如果 `raw/`、`notes/`、`evidence/` 的证据材料发生变化,必须先重新运行 `build_parallel_review_plan.py`,让输入快照和 `project_id` 同步刷新。 关键产物: - `parallel_review/parallel_review_job.json` - `parallel_review/parallel_plan.json` - `parallel_review/parallel_plan.md` - `parallel_review/parallel_runs/.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/parallel-vibe/<project_id>/...` - `parallel_review/independent_review_summary.md` - `parallel_review/independent_review_summary.json` 最终报告应引用: - 独立评审的 recommendation / risk 分布 - 主要共识 - 主要分歧 - 若涉及依赖/第三方代码,还应引用各 thread 对 license 风险的判断 - 这些分歧是否改变最终 merge 建议 -
report-template.md 1.2 KB
# Git PR Review Report Template ```markdown # Git PR Review: {owner/repo} #{pr_number} ## 结论摘要 - 建议:Merge / Merge after fixes / Request changes / Do not merge / Escalate security review - 风险等级:Low / Medium / High / Critical - 一句话判断:... ## 独立评审综合结果 - 独立评审次数:5(或用户指定值) - recommendation 分布:... - risk 分布:... - 共识:... - 分歧:... ## PR 在解决什么问题 - 背景:... - 证据:... ## 方案分析 ### 优势 - ... ### 局限 - ... ### 替代方案或待确认点 - ... ## 恶意/安全风险审查 - 结论:... - 风险信号:... - 证据:... ## License / 合规审查 - 结论:无明显风险 / 存在待确认风险 / 明显冲突 - 风险来源:依赖 / vendored 代码 / 资源资产 / 文件头 / 许可证文件 - 建议动作:补声明 / 替换依赖 / 法务确认 / 不建议 merge ## 与“好 PR”社区标准的对照 - 标准 + 链接 + 本 PR 表现 ## 关键证据 - Diff:... - 评论:... - CI:... - Issue / Docs:... - 外部参考:... ## 证据不足与待确认点 - 缺失材料:... - 对结论的影响:... ## 建议的处理方式 - ... ``` -
security-checklist.md 1.3 KB
# 恶意 / 高风险 PR 审查清单 优先检查以下信号: 1. 凭证与敏感数据 - 是否新增读取环境变量、token、cookie、secret 的逻辑 - 是否把敏感信息发到外部域名、日志或第三方服务 2. 远程执行与下载 - 是否新增 `curl | sh`、动态下载脚本、远程模板注入、反序列化执行 - 是否把本来静态的流程改成了运行远端内容 3. 权限与发布链路 - 是否修改 CI/CD、发布脚本、权限文件、密钥注入路径、workflow 触发条件 - 是否增加了管理员、写权限、部署权限、包发布权限 - 是否替换 GitHub Actions / CI 里的第三方 action 版本、下载源或 runner 行为 4. 防护削弱 - 是否删掉测试、告警、审计日志、输入校验、签名验证、安全开关 - 是否把严格校验改成“默默放行” 5. 隐蔽性 - 是否引入混淆代码、难以解释的编码载荷、可疑依赖、异常大的二进制文件 - 是否把危险逻辑藏进“cleanup / refactor / docs / test helper”这类无害命名里 6. 供应链与依赖 - 是否升级/替换依赖到来路不明的镜像、fork、私有源或动态下载地址 - 是否修改包管理器锁文件却没有对应的合理说明 7. 社会工程 - 改动标题、描述、提交信息与真实 diff 是否不一致 - 改动范围是否显著偏离 PR 自称目标
-
-
scripts
-
aggregate_parallel_reviews.py 9.1 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys from collections import Counter from pathlib import Path from typing import Any from common_config import get_skill_root, load_config SCRIPT_PATH = Path(__file__) SKILL_ROOT = get_skill_root(SCRIPT_PATH) CONFIG = load_config(SKILL_ROOT) PARALLEL_CFG = CONFIG["parallel_review"] REVIEW_POLICY = CONFIG.get("review_policy") or {} RESULT_FILENAME = str(PARALLEL_CFG["result_filename"]) REQUIRED_RESULT_SECTIONS = [str(item) for item in (PARALLEL_CFG.get("required_result_sections") or [])] ALLOWED_RECOMMENDATIONS = {str(item) for item in (REVIEW_POLICY.get("final_recommendations") or [])} ALLOWED_RISK_LEVELS = {str(item) for item in (REVIEW_POLICY.get("risk_levels") or [])} ALLOWED_CONFIDENCE_LEVELS = {str(item) for item in (PARALLEL_CFG.get("confidence_levels") or ["High", "Medium", "Low"])} REC_RE = re.compile(r"^- Recommendation:\s*(.+?)\s*$", re.MULTILINE) RISK_RE = re.compile(r"^- Risk Level:\s*(.+?)\s*$", re.MULTILINE) CONF_RE = re.compile(r"^- Confidence:\s*(.+?)\s*$", re.MULTILINE) def fail(message: str) -> int: print(f"ERROR: {message}", file=sys.stderr) return 1 def _load_job(path: Path) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise ValueError(f"job file is not valid JSON: {path}") from exc if not isinstance(payload, dict): raise ValueError("job file must be a JSON object") return payload def _read_text(path: Path) -> str: return path.read_text(encoding="utf-8", errors="replace") def _extract(pattern: re.Pattern[str], text: str) -> str: m = pattern.search(text) return m.group(1).strip() if m else "Unknown" def _expected_thread_ids(review_count: int) -> list[str]: width = int(PARALLEL_CFG["thread_id_width"]) return [str(index).zfill(width) for index in range(1, review_count + 1)] def _validate_result_text(text: str) -> list[str]: issues: list[str] = [] for section in REQUIRED_RESULT_SECTIONS: if section not in text: issues.append(f"missing required section {section!r}") recommendation = _extract(REC_RE, text) risk_level = _extract(RISK_RE, text) confidence = _extract(CONF_RE, text) if recommendation == "Unknown": issues.append("missing Recommendation field") elif ALLOWED_RECOMMENDATIONS and recommendation not in ALLOWED_RECOMMENDATIONS: issues.append(f"invalid Recommendation: {recommendation}") if risk_level == "Unknown": issues.append("missing Risk Level field") elif ALLOWED_RISK_LEVELS and risk_level not in ALLOWED_RISK_LEVELS: issues.append(f"invalid Risk Level: {risk_level}") if confidence == "Unknown": issues.append("missing Confidence field") elif ALLOWED_CONFIDENCE_LEVELS and confidence not in ALLOWED_CONFIDENCE_LEVELS: issues.append(f"invalid Confidence: {confidence}") return issues def _thread_entries(project_root: Path, expected_thread_ids: list[str]) -> tuple[list[dict[str, Any]], list[str], list[str]]: entries: list[dict[str, Any]] = [] missing_threads: list[str] = [] invalid_threads: list[str] = [] for thread_id in expected_thread_ids: child = project_root / thread_id if not child.is_dir(): missing_threads.append(f"{thread_id} (missing thread directory)") continue result_path = child / RESULT_FILENAME if not result_path.exists(): missing_threads.append(f"{thread_id} (missing {RESULT_FILENAME})") continue text = _read_text(result_path) thread_json = child / "thread.json" title = child.name if thread_json.exists(): try: title = json.loads(thread_json.read_text(encoding="utf-8")).get("title") or title except Exception: pass issues = _validate_result_text(text) if issues: invalid_threads.append(f"{thread_id} ({'; '.join(issues)})") continue entries.append( { "thread_id": child.name, "title": title, "result_path": str(result_path), "recommendation": _extract(REC_RE, text), "risk_level": _extract(RISK_RE, text), "confidence": _extract(CONF_RE, text), "excerpt": "\n".join(line for line in text.splitlines()[:20]).strip(), } ) return entries, missing_threads, invalid_threads def _mode_or_mixed(values: list[str]) -> str: filtered = [v for v in values if v and v != "Unknown"] if not filtered: return "Unknown" counts = Counter(filtered).most_common() if len(counts) > 1 and counts[0][1] == counts[1][1]: return "Mixed" return counts[0][0] def main() -> int: parser = argparse.ArgumentParser(description="Aggregate parallel-vibe independent PR review results.") parser.add_argument("--job-file", required=True, help="Path to parallel_review_job.json") args = parser.parse_args() job_path = Path(args.job_file).expanduser().resolve() if not job_path.exists(): return fail(f"job file not found: {job_path}") try: job = _load_job(job_path) except ValueError as exc: return fail(str(exc)) for key in ("project_root", "aggregate_markdown", "aggregate_json", "review_count"): if key not in job: return fail(f"job file missing required field: {key}") project_root = Path(job["project_root"]).resolve() aggregate_md = Path(job["aggregate_markdown"]).resolve() aggregate_json = Path(job["aggregate_json"]).resolve() review_count = int(job["review_count"]) if review_count < 1: return fail(f"review_count must be >= 1: {review_count}") if not project_root.exists() or not project_root.is_dir(): recommended = job.get("recommended_command") hint = f"; run `{recommended}` first" if recommended else "" return fail(f"parallel review project root not found: {project_root}{hint}") entries, missing_threads, invalid_threads = _thread_entries(project_root, _expected_thread_ids(review_count)) if missing_threads: return fail("parallel review outputs are incomplete: " + ", ".join(missing_threads)) if invalid_threads: return fail("parallel review outputs are invalid: " + ", ".join(invalid_threads)) if not entries: return fail(f"no {RESULT_FILENAME} files found under {project_root}") rec_counts = Counter(entry["recommendation"] for entry in entries) risk_counts = Counter(entry["risk_level"] for entry in entries) consensus = { "recommendation": _mode_or_mixed([entry["recommendation"] for entry in entries]), "risk_level": _mode_or_mixed([entry["risk_level"] for entry in entries]), } payload = { "expected_review_count": review_count, "review_count": len(entries), "project_root": str(project_root), "consensus": consensus, "recommendation_counts": dict(rec_counts), "risk_counts": dict(risk_counts), "threads": entries, } aggregate_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") md_lines = [ "# Independent Review Summary", "", f"- Expected Review Count: {review_count}", f"- Review Count: {len(entries)}", f"- Project Root: `{project_root}`", f"- Consensus Recommendation: {consensus['recommendation']}", f"- Consensus Risk Level: {consensus['risk_level']}", "", "## Recommendation Distribution", ] for key, value in sorted(rec_counts.items()): md_lines.append(f"- {key}: {value}") md_lines.extend(["", "## Risk Distribution"]) for key, value in sorted(risk_counts.items()): md_lines.append(f"- {key}: {value}") md_lines.extend(["", "## Review Matrix", "| Thread | Title | Recommendation | Risk | Confidence |", "|------|------|------|------|------|"]) for entry in entries: md_lines.append( f"| {entry['thread_id']} | {entry['title']} | {entry['recommendation']} | {entry['risk_level']} | {entry['confidence']} |" ) md_lines.extend(["", "## Per-Thread Excerpts"]) for entry in entries: md_lines.extend([ f"### Thread {entry['thread_id']} - {entry['title']}", f"- Result Path: `{entry['result_path']}`", "", "```markdown", entry["excerpt"], "```", "", ]) md_lines.extend([ "## Guidance For Final Report", "- 在最终 `Git-PR-Review_*.md` 中新增 `## 独立评审综合结果` 章节。", "- 先报告 recommendation / risk 的分布,再写共识与分歧。", "- 若共识为 `Mixed`,必须明确说明主要分歧点。", ]) aggregate_md.write_text("\n".join(md_lines).rstrip() + "\n", encoding="utf-8") print(json.dumps({"aggregate_markdown": str(aggregate_md), "aggregate_json": str(aggregate_json)}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main()) -
build_parallel_review_plan.py 14.5 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import shlex import shutil import sys from pathlib import Path from textwrap import dedent from typing import Any from common_config import get_skill_root, load_config SCRIPT_PATH = Path(__file__) SKILL_ROOT = get_skill_root(SCRIPT_PATH) CONFIG = load_config(SKILL_ROOT) DIRECTORIES_CFG = CONFIG["directories"] PARALLEL_CFG = CONFIG["parallel_review"] FILES_CFG = CONFIG["files"] DEPENDENCIES_CFG = CONFIG.get("dependencies") or {} REVIEW_POLICY = CONFIG.get("review_policy") or {} SNAPSHOT_SOURCES = ( ("raw_dir", str(DIRECTORIES_CFG["raw"])), ("notes_dir", str(DIRECTORIES_CFG["notes"])), ("evidence_dir", str(DIRECTORIES_CFG["evidence"])), ) def fail(message: str) -> int: print(f"ERROR: {message}", file=sys.stderr) return 1 def _ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) def _copy_if_exists(src: Path, dst: Path) -> None: if not src.exists(): return if src.is_dir(): if dst.exists(): shutil.rmtree(dst) shutil.copytree(src, dst) else: _ensure_dir(dst.parent) shutil.copy2(src, dst) def _digest_tree(root: Path) -> str: digest = hashlib.md5() for path in sorted(p for p in root.rglob("*") if p.is_file()): rel = path.relative_to(root).as_posix().encode("utf-8") digest.update(rel) digest.update(b"\0") digest.update(path.read_bytes()) digest.update(b"\0") return digest.hexdigest() def _resolve_parallel_vibe_script() -> Path: configured = str(DEPENDENCIES_CFG.get("parallel_vibe_script") or "").strip() if not configured: raise ValueError("config.dependencies.parallel_vibe_script is required") candidate = Path(configured).expanduser() if not candidate.is_absolute(): candidate = (SKILL_ROOT / candidate).resolve() else: candidate = candidate.resolve() if not candidate.exists() or not candidate.is_file(): raise ValueError(f"parallel-vibe runner script not found: {candidate}") return candidate def _result_template_text() -> str: sections = [str(item) for item in (PARALLEL_CFG.get("required_result_sections") or [])] if not sections: sections = [ "# Independent PR Review", "## Final Call", "## Problem Understanding", "## Strengths", "## Limitations", "## Security Review", "## License Review", "## Good PR Criteria Comparison", "## Key Evidence", "## Evidence Gaps", "## Suggested Disposition", ] recommendations = " / ".join(str(item) for item in (REVIEW_POLICY.get("final_recommendations") or [])) risks = " / ".join(str(item) for item in (REVIEW_POLICY.get("risk_levels") or [])) confidences = " / ".join(str(item) for item in (PARALLEL_CFG.get("confidence_levels") or ["High", "Medium", "Low"])) lines: list[str] = [sections[0], ""] for section in sections[1:]: lines.append(section) if section == "## Final Call": lines.extend( [ f"- Recommendation: {recommendations}", f"- Risk Level: {risks}", f"- Confidence: {confidences}", ] ) else: lines.append("- ...") lines.append("") return "\n".join(lines).rstrip() def _load_manifest(path: Path) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise ValueError(f"manifest is not valid JSON: {path}") from exc if not isinstance(payload, dict): raise ValueError("manifest must be a JSON object") return payload def _require_mapping(payload: dict[str, Any], key: str) -> dict[str, Any]: value = payload.get(key) if not isinstance(value, dict): raise ValueError(f"manifest missing object: {key}") return value def _resolve_path(value: Any, *, field_name: str) -> Path: if not isinstance(value, str) or not value.strip(): raise ValueError(f"manifest missing path: {field_name}") return Path(value).expanduser().resolve() def _validate_manifest(manifest: dict[str, Any]) -> tuple[Path, dict[str, Path]]: repo = _require_mapping(manifest, "repo") pull_request = _require_mapping(manifest, "pull_request") if not str(repo.get("owner") or "").strip() or not str(repo.get("name") or "").strip(): raise ValueError("manifest.repo must include owner and name") if not str(pull_request.get("input") or pull_request.get("pr_slug") or "").strip(): raise ValueError("manifest.pull_request must include input or pr_slug") paths = _require_mapping(manifest, "paths") run_dir = _resolve_path(paths.get("run_dir"), field_name="paths.run_dir") if not run_dir.exists() or not run_dir.is_dir(): raise ValueError(f"manifest paths.run_dir does not exist: {run_dir}") resolved: dict[str, Path] = {"run_dir": run_dir} for key, _ in SNAPSHOT_SOURCES: current = _resolve_path(paths.get(key), field_name=f"paths.{key}") if not current.exists() or not current.is_dir(): raise ValueError(f"manifest {key} does not exist: {current}") try: current.relative_to(run_dir) except ValueError as exc: raise ValueError(f"manifest {key} must stay inside run_dir: {current}") from exc resolved[key] = current return run_dir, resolved def _review_prompt(*, thread_id: str, idx: int, total: int, repo: str, pr_ref: str, focus: str, extra: str) -> str: extra_block = f"\n补充指示:\n{extra.strip()}\n" if extra.strip() else "" result_template = _result_template_text() return dedent( f""" 你正在执行一次 GitHub Pull Request 的独立评审。 这是第 {idx}/{total} 个独立评审 thread,thread_id={thread_id}。 你的任务不是参考其他 thread,而是基于当前 workspace 中提供的材料,独立完成一次完整 PR 审查。 目标仓库:{repo} 目标 PR:{pr_ref} 本 thread 的次级关注焦点:{focus} {extra_block} 你必须遵守以下要求: - 只能基于当前 workspace 中已有的 `manifest.json`、`raw/`、`notes/`、`evidence/` 材料进行判断。 - 不要假设你能看到其他 thread 的结果。 - 不要修改原始证据文件;只在当前工作区根目录写出 `RESULT.md`。 - 如果材料不足,要明确写出“证据不足”与它对结论的影响。 - 如果 PR 涉及依赖、复制的第三方代码、字体、图标、模板、数据或许可证文件,必须显式给出 license / 合规判断。 - 优先使用 workspace 中 `notes/community_good_pr.md` 的内置“好 PR”标准。 - 只有用户明确要求最新口径、指定特殊社区规范,或你判断当前内置标准不足时,才额外联网补充来源;如补充了,必须在 RESULT.md 里说明。 `RESULT.md` 必须使用以下结构: {result_template} """ ).strip() + "\n" def _build_plan( *, manifest: dict[str, Any], review_count: int, extra_instructions: str, review_runner_type: str, review_runner_profile: str, ) -> dict[str, Any]: thread_width = int(PARALLEL_CFG["thread_id_width"]) repo = manifest["repo"] pr = manifest["pull_request"] repo_ref = f"{repo['owner']}/{repo['name']}" pr_ref = str(pr.get("input") or pr.get("pr_slug") or pr.get("number") or "") focuses = list(PARALLEL_CFG.get("review_focuses") or []) threads: list[dict[str, Any]] = [] for idx in range(1, review_count + 1): thread_id = str(idx).zfill(thread_width) focus = focuses[(idx - 1) % len(focuses)] if focuses else "完整独立评审" threads.append( { "thread_id": thread_id, "title": f"Independent Review {idx}", "runner": { "type": review_runner_type, "profile": review_runner_profile, "model": "", "args": [], }, "prompt": _review_prompt( thread_id=thread_id, idx=idx, total=review_count, repo=repo_ref, pr_ref=pr_ref, focus=focus, extra=extra_instructions, ), } ) synthesis_cfg = PARALLEL_CFG.get("synthesis") or {} synthesis_runner = synthesis_cfg.get("runner") or {"type": "claude", "profile": "deep"} return { "plan_version": 1, "prompt": f"Independent PR review for {repo_ref} {pr_ref}", "threads": threads, "synthesis": { "enabled": bool(synthesis_cfg.get("enabled", False)), "runner": { "type": str(synthesis_runner.get("type") or "claude"), "profile": str(synthesis_runner.get("profile") or "deep"), "model": "", "args": [], }, "prompt": "请综合多份独立 PR 审查结果,生成一份最终决策建议。", }, } def main() -> int: parser = argparse.ArgumentParser(description="Build a parallel-vibe plan for independent git-pr-review runs.") parser.add_argument("--manifest", required=True, help="Path to git-pr-review manifest.json") parser.add_argument("--n", type=int, default=int(PARALLEL_CFG["default_review_count"]), help="Independent review count") parser.add_argument("--extra-instructions", default="", help="Extra instructions passed to all independent reviewers") parser.add_argument( "--review-runner-type", default=str(PARALLEL_CFG["review_runner"]["type"]), help="Runner type for independent reviewers (default from config.yaml)", ) parser.add_argument( "--review-runner-profile", default=str(PARALLEL_CFG["review_runner"]["profile"]), help="Runner profile for independent reviewers (default from config.yaml)", ) args = parser.parse_args() manifest_path = Path(args.manifest).expanduser().resolve() if not manifest_path.exists(): return fail(f"manifest not found: {manifest_path}") review_count = int(args.n) if review_count < int(PARALLEL_CFG["min_review_count"]) or review_count > int(PARALLEL_CFG["max_review_count"]): return fail( f"--n must be within [{PARALLEL_CFG['min_review_count']}, {PARALLEL_CFG['max_review_count']}]: {review_count}" ) try: manifest = _load_manifest(manifest_path) run_dir, manifest_paths = _validate_manifest(manifest) except ValueError as exc: return fail(str(exc)) parallel_root = run_dir / str(PARALLEL_CFG["workspace_dir"]) input_snapshot = parallel_root / str(PARALLEL_CFG["input_snapshot_dir"]) parallel_out_dir = parallel_root / str(PARALLEL_CFG["parallel_out_dir"]) plan_path = parallel_root / str(PARALLEL_CFG["plan_filename"]) plan_md_path = parallel_root / str(PARALLEL_CFG["plan_markdown"]) job_path = parallel_root / str(PARALLEL_CFG["job_manifest_name"]) aggregate_md_path = parallel_root / str(PARALLEL_CFG["aggregate_markdown"]) aggregate_json_path = parallel_root / str(PARALLEL_CFG["aggregate_json"]) _ensure_dir(parallel_root) _ensure_dir(parallel_out_dir) if input_snapshot.exists(): shutil.rmtree(input_snapshot) _ensure_dir(input_snapshot) _copy_if_exists(manifest_path, input_snapshot / str(FILES_CFG["manifest_name"])) for path_key, dest_dirname in SNAPSHOT_SOURCES: _copy_if_exists(manifest_paths[path_key], input_snapshot / dest_dirname) snapshot_fingerprint = _digest_tree(input_snapshot) plan = _build_plan( manifest=manifest, review_count=review_count, extra_instructions=str(args.extra_instructions or ""), review_runner_type=str(args.review_runner_type or PARALLEL_CFG["review_runner"]["type"]), review_runner_profile=str(args.review_runner_profile or PARALLEL_CFG["review_runner"]["profile"]), ) plan_bytes = json.dumps(plan, ensure_ascii=False, indent=2).encode("utf-8") + b"\n" plan_path.write_bytes(plan_bytes) project_id = hashlib.md5(plan_bytes + snapshot_fingerprint.encode("utf-8")).hexdigest() project_root = parallel_out_dir / ".bensz-api" / "skills" / "parallel-vibe" / project_id try: parallel_vibe_script = _resolve_parallel_vibe_script() except ValueError as exc: return fail(str(exc)) command = " ".join( [ "python3", shlex.quote(str(parallel_vibe_script)), "--plan-file", shlex.quote(str(plan_path)), "--src-dir", shlex.quote(str(input_snapshot)), "--out-dir", shlex.quote(str(parallel_out_dir)), "--project-id", shlex.quote(project_id), ] ) plan_md_path.write_text( dedent( f""" # Parallel Independent Review Plan - Review count: {review_count} - Manifest: `{manifest_path}` - Input snapshot: `{input_snapshot}` - Input snapshot fingerprint: `{snapshot_fingerprint}` - Parallel out dir: `{parallel_out_dir}` - Parallel runner script: `{parallel_vibe_script}` - Expected project root: `{project_root}` - Aggregate markdown: `{aggregate_md_path}` - Aggregate json: `{aggregate_json_path}` - Recommended command: `{command}` """ ).strip() + "\n", encoding="utf-8", ) job = { "review_count": review_count, "manifest": str(manifest_path), "run_dir": str(run_dir), "parallel_root": str(parallel_root), "input_snapshot": str(input_snapshot), "input_snapshot_fingerprint": snapshot_fingerprint, "parallel_out_dir": str(parallel_out_dir), "parallel_vibe_script": str(parallel_vibe_script), "plan_file": str(plan_path), "plan_markdown": str(plan_md_path), "project_id": project_id, "project_root": str(project_root), "aggregate_markdown": str(aggregate_md_path), "aggregate_json": str(aggregate_json_path), "recommended_command": command, } job_path.write_text(json.dumps(job, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps(job, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main()) -
common_config.py 4.2 KB
#!/usr/bin/env python3 from __future__ import annotations from pathlib import Path from typing import Any def _strip_comment(text: str) -> str: if "#" not in text: return text.rstrip() in_quote = False quote_char = "" out: list[str] = [] for ch in text: if ch in {'"', "'"}: if in_quote and ch == quote_char: in_quote = False quote_char = "" elif not in_quote: in_quote = True quote_char = ch if ch == "#" and not in_quote: break out.append(ch) return "".join(out).rstrip() def _parse_scalar(value: str) -> Any: value = _strip_comment(value).strip() if value == "": return "" if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")): return value[1:-1] if value.lower() == "true": return True if value.lower() == "false": return False if value.isdigit(): return int(value) return value def _indent_of(line: str) -> int: return len(line) - len(line.lstrip(" ")) def _next_meaningful(lines: list[str], index: int) -> int: while index < len(lines): stripped = _strip_comment(lines[index]).strip() if stripped: return index index += 1 return index def _parse_block(lines: list[str], index: int, indent: int) -> tuple[Any, int]: index = _next_meaningful(lines, index) if index >= len(lines): return {}, index stripped = _strip_comment(lines[index]).strip() if stripped.startswith("- "): result: list[Any] = [] while index < len(lines): index = _next_meaningful(lines, index) if index >= len(lines): break line = lines[index] current_indent = _indent_of(line) stripped = _strip_comment(line).strip() if current_indent < indent or not stripped.startswith("- "): break item_text = stripped[2:].strip() if item_text: result.append(_parse_scalar(item_text)) index += 1 continue child, index = _parse_block(lines, index + 1, current_indent + 2) result.append(child) return result, index result_dict: dict[str, Any] = {} while index < len(lines): index = _next_meaningful(lines, index) if index >= len(lines): break line = lines[index] current_indent = _indent_of(line) if current_indent < indent: break if current_indent > indent: raise ValueError(f"Unexpected indentation at line {index + 1}: {line!r}") stripped = _strip_comment(line).strip() if stripped.startswith("- "): raise ValueError(f"Unexpected list item at line {index + 1}: {line!r}") key, sep, rest = stripped.partition(":") if not sep: raise ValueError(f"Invalid line {index + 1}: {line!r}") key = key.strip() rest = rest.strip() if rest: result_dict[key] = _parse_scalar(rest) index += 1 continue next_index = _next_meaningful(lines, index + 1) if next_index >= len(lines) or _indent_of(lines[next_index]) <= current_indent: result_dict[key] = {} index = next_index continue child, index = _parse_block(lines, next_index, _indent_of(lines[next_index])) result_dict[key] = child return result_dict, index def load_config(skill_root: Path) -> dict[str, Any]: config_path = skill_root / "config.yaml" text = config_path.read_text(encoding="utf-8") try: import yaml # type: ignore except Exception: yaml = None # type: ignore if yaml is not None: try: data = yaml.safe_load(text) if isinstance(data, dict): return data except Exception: pass parsed, _ = _parse_block(text.splitlines(), 0, 0) if not isinstance(parsed, dict): raise ValueError("config.yaml must parse to a mapping") return parsed def get_skill_root(script_path: Path) -> Path: return script_path.resolve().parent.parent -
prepare_review_job.py 9.4 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys from datetime import datetime from pathlib import Path from urllib.parse import urlparse from common_config import get_skill_root, load_config SCRIPT_PATH = Path(__file__) SKILL_ROOT = get_skill_root(SCRIPT_PATH) CONFIG = load_config(SKILL_ROOT) DIRECTORIES = CONFIG["directories"] FILES = CONFIG["files"] OUTPUT = CONFIG["output"] GOOD_PR_REFERENCE = SKILL_ROOT / "references" / "good-pr-standards.md" PR_NUMBER_RE = re.compile(r"^(?:#|pr[-\s]?)?(\d+)$", re.IGNORECASE) def sanitize(text: str) -> str: text = re.sub(r"[^A-Za-z0-9._-]+", "_", text.strip()) text = re.sub(r"_+", "_", text).strip("._-") return text or "unknown" def validate_timestamp(timestamp: str) -> str: fmt = str(OUTPUT["timestamp_format"]) try: datetime.strptime(timestamp, fmt) except ValueError as exc: raise ValueError(f"timestamp must match {fmt!r}: {timestamp}") from exc return timestamp def parse_repo(repo: str) -> tuple[str, str, str]: repo = repo.strip() if repo.startswith(("http://", "https://")): parsed = urlparse(repo) if parsed.netloc not in {"github.com", "www.github.com"}: raise ValueError(f"Only github.com repositories are supported: {repo}") parts = [p for p in parsed.path.split("/") if p] if len(parts) != 2: raise ValueError(f"Repository URL must point to the repo root: {repo}") owner, name = parts[0], parts[1] else: parts = [p for p in repo.split("/") if p] if len(parts) != 2: raise ValueError("Repository must be a GitHub URL or owner/repo slug.") owner, name = parts[0], parts[1] repo_name = name.removesuffix(".git") if not owner or not repo_name: raise ValueError("Repository must include both owner and repo name.") repo_slug = sanitize(f"{owner}_{repo_name}") return owner, repo_name, repo_slug def parse_pr(pr: str) -> tuple[str, int, tuple[str, str] | None]: pr = pr.strip() if pr.startswith(("http://", "https://")): parsed = urlparse(pr) if parsed.netloc not in {"github.com", "www.github.com"}: raise ValueError(f"Only github.com PR URLs are supported: {pr}") parts = [p for p in parsed.path.split("/") if p] if len(parts) >= 4 and parts[2] == "pull" and parts[3].isdigit(): number = int(parts[3]) return f"pr-{number}", number, (parts[0], parts[1]) raise ValueError(f"Could not parse PR number from URL: {pr}") match = PR_NUMBER_RE.fullmatch(pr) if not match: raise ValueError("PR must be a PR URL, '#123', '123', or 'pr-123'.") number = int(match.group(1)) return f"pr-{number}", number, None def ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) def allocate_run_dir(workspace_root: Path, timestamp: str) -> tuple[Path, str]: candidate = workspace_root / timestamp if not candidate.exists(): return candidate, timestamp for idx in range(2, 100): run_id = f"{timestamp}-{idx:02d}" candidate = workspace_root / run_id if not candidate.exists(): return candidate, run_id raise RuntimeError(f"failed to allocate unique run directory under {workspace_root}") def write_placeholder(path: Path, content: str) -> None: if not path.exists(): path.write_text(content, encoding="utf-8") def build_good_pr_note() -> str: if not GOOD_PR_REFERENCE.exists(): return "# Community Good PR Notes\n\n- 未找到内置参考 `references/good-pr-standards.md`,请手动补充。\n" source = GOOD_PR_REFERENCE.read_text(encoding="utf-8").strip() return ( "# Community Good PR Notes\n\n" "以下内容由 skill 内置参考 `references/good-pr-standards.md` 预填充;" "默认优先使用这些标准,不必每次实时联网。\n\n" + source + "\n" ) def main() -> int: parser = argparse.ArgumentParser(description="Prepare an isolated workspace for git-pr-review.") parser.add_argument("--repo", required=True, help="GitHub repo URL or owner/repo") parser.add_argument("--pr", required=True, help="PR URL or PR number") parser.add_argument("--workspace-dir", default=str(DIRECTORIES["default_workspace"]), help="Workspace root") parser.add_argument("--report-dir", default=str(OUTPUT["default_report_dir"]), help="Final report directory") parser.add_argument("--timestamp", default="", help="Override timestamp") args = parser.parse_args() try: owner, repo_name, repo_slug = parse_repo(args.repo) pr_slug, pr_number, pr_repo = parse_pr(args.pr) if pr_repo is not None and tuple(pr_repo) != (owner, repo_name): raise ValueError( f"PR URL points to {pr_repo[0]}/{pr_repo[1]}, but --repo points to {owner}/{repo_name}." ) fmt = str(OUTPUT["timestamp_format"]) timestamp = validate_timestamp(args.timestamp) if args.timestamp else datetime.now().strftime(fmt) except ValueError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 workspace_root = Path(args.workspace_dir).expanduser() report_root = Path(args.report_dir).expanduser() if not workspace_root.is_absolute(): workspace_root = (Path.cwd() / workspace_root).resolve() else: workspace_root = workspace_root.resolve() if not report_root.is_absolute(): report_root = (Path.cwd() / report_root).resolve() else: report_root = report_root.resolve() try: run_dir, run_id = allocate_run_dir(workspace_root, f"{DIRECTORIES['run_prefix']}{timestamp}") except RuntimeError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 raw_dir = run_dir / str(DIRECTORIES["raw"]) notes_dir = run_dir / str(DIRECTORIES["notes"]) evidence_dir = run_dir / str(DIRECTORIES["evidence"]) logs_dir = run_dir / str(DIRECTORIES["logs"]) for path in (workspace_root, run_dir, raw_dir, notes_dir, evidence_dir, logs_dir, report_root): ensure_dir(path) write_placeholder( raw_dir / str(FILES["raw_readme"]), "# Raw Inputs\n\n把 PR 元数据、diff、评论、CI 状态与关联 issue 保存到这个目录。\n", ) write_placeholder( notes_dir / str(FILES["user_context_note"]), "# User Context\n\n- 用户已有判断:\n- 关注点:\n- 禁区或限制:\n", ) write_placeholder( notes_dir / str(FILES["community_note"]), build_good_pr_note(), ) write_placeholder( notes_dir / str(FILES["license_review_note"]), "# License Review\n\n- 是否涉及新依赖、复制代码、字体、图标、模板、数据或模型资源:\n- 发现的 license / notice:\n- 是否存在兼容性风险:\n- 建议动作:\n", ) write_placeholder( evidence_dir / str(FILES["key_findings_note"]), "# Key Findings\n\n- 关键发现 1:\n- 关键发现 2:\n", ) write_placeholder( evidence_dir / str(FILES["missing_items_note"]), "# Missing Items\n\n- 未获取材料:\n- 原因:\n- 对结论的影响:\n", ) report_name = f"{OUTPUT['report_prefix']}_{repo_slug}_{pr_slug}_{run_id}{OUTPUT['report_extension']}" manifest_path = run_dir / str(FILES["manifest_name"]) manifest = { "generated_at": timestamp, "run_id": run_id, "repo": { "input": args.repo, "owner": owner, "name": repo_name, "repo_slug": repo_slug, "skill_version": str(CONFIG["skill_info"]["version"]), }, "pull_request": { "input": args.pr, "pr_slug": pr_slug, "number": pr_number, "url_repo": list(pr_repo) if pr_repo is not None else None, }, "paths": { "workspace_root": str(workspace_root), "run_dir": str(run_dir), "raw_dir": str(raw_dir), "notes_dir": str(notes_dir), "evidence_dir": str(evidence_dir), "logs_dir": str(logs_dir), "report_dir": str(report_root), "report_path": str(report_root / report_name), }, "files": { "manifest_name": str(FILES["manifest_name"]), "manifest_path": str(manifest_path), "raw_readme": str(raw_dir / str(FILES["raw_readme"])), "user_context_note": str(notes_dir / str(FILES["user_context_note"])), "community_note": str(notes_dir / str(FILES["community_note"])), "license_review_note": str(notes_dir / str(FILES["license_review_note"])), "key_findings_note": str(evidence_dir / str(FILES["key_findings_note"])), "missing_items_note": str(evidence_dir / str(FILES["missing_items_note"])), }, "policy": { "read_only": True, "default_hidden_workspace": args.workspace_dir == str(DIRECTORIES["default_workspace"]), "must_keep_intermediate_inside_workspace": True, "required_report_sections": list(OUTPUT["required_sections"]), }, } manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") print(json.dumps({ "manifest": str(manifest_path), "run_dir": str(run_dir), "report_path": str(report_root / report_name), }, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main()) -
validate_review_artifacts.py 5.1 KB
#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys from pathlib import Path from typing import Any from common_config import get_skill_root, load_config SCRIPT_PATH = Path(__file__) SKILL_ROOT = get_skill_root(SCRIPT_PATH) CONFIG = load_config(SKILL_ROOT) OUTPUT = CONFIG["output"] REQUIRED_PATH_KEYS = [ "workspace_root", "run_dir", "raw_dir", "notes_dir", "evidence_dir", "logs_dir", "report_dir", "report_path", ] REQUIRED_FILE_KEYS = [ "manifest_path", "raw_readme", "user_context_note", "community_note", "license_review_note", "key_findings_note", "missing_items_note", ] def fail(message: str) -> int: print(f"ERROR: {message}", file=sys.stderr) return 1 def build_report_regex() -> re.Pattern[str]: prefix = re.escape(str(OUTPUT["report_prefix"])) ext = re.escape(str(OUTPUT["report_extension"])) return re.compile( rf"^{prefix}_[A-Za-z0-9._-]+_pr-\d+_\d{{4}}-\d{{2}}-\d{{2}}-\d{{2}}-\d{{2}}(?:-\d{{2}})?{ext}$" ) def _load_manifest(path: Path) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise ValueError(f"manifest is not valid JSON: {path}") from exc if not isinstance(payload, dict): raise ValueError("manifest must be a JSON object") return payload def _require_mapping(payload: dict[str, Any], key: str) -> dict[str, Any]: value = payload.get(key) if not isinstance(value, dict): raise ValueError(f"manifest missing object: {key}") return value def _resolve_manifest_paths(paths: dict[str, Any]) -> dict[str, Path]: resolved: dict[str, Path] = {} for key in REQUIRED_PATH_KEYS: value = paths.get(key) if not isinstance(value, str) or not value.strip(): raise ValueError(f"manifest missing paths.{key}") resolved[key] = Path(value).expanduser().resolve() return resolved def main() -> int: parser = argparse.ArgumentParser(description="Validate git-pr-review workspace and report artifacts.") parser.add_argument("--manifest", required=True, help="Path to manifest.json") parser.add_argument("--report", required=True, help="Path to final Markdown report") args = parser.parse_args() manifest_path = Path(args.manifest).expanduser().resolve() report_path = Path(args.report).expanduser().resolve() if not manifest_path.exists(): return fail(f"manifest not found: {manifest_path}") if not report_path.exists(): return fail(f"report not found: {report_path}") try: manifest = _load_manifest(manifest_path) paths = _require_mapping(manifest, "paths") files = _require_mapping(manifest, "files") policy = manifest.get("policy", {}) if not isinstance(policy, dict): return fail("manifest.policy must be a JSON object") resolved = _resolve_manifest_paths(paths) except ValueError as exc: return fail(str(exc)) for key in REQUIRED_FILE_KEYS: if key not in files: return fail(f"manifest missing files.{key}") run_dir = resolved["run_dir"] workspace_root = resolved["workspace_root"] for key in ("raw_dir", "notes_dir", "evidence_dir", "logs_dir"): path = resolved[key] if not path.exists() or not path.is_dir(): return fail(f"required directory missing: {path}") try: path.relative_to(run_dir) except ValueError: return fail(f"{key} must be inside run_dir: {path}") try: run_dir.relative_to(workspace_root) except ValueError: return fail("run_dir must be inside workspace_root") if OUTPUT.get("enforce_hidden_workspace_when_default") and policy.get("default_hidden_workspace"): if ".bensz-api" not in workspace_root.parts and not workspace_root.name.startswith("."): return fail("default workspace must be inside a hidden directory") for key in REQUIRED_FILE_KEYS: value = files[key] path = Path(value).expanduser().resolve() if not path.exists() or not path.is_file(): return fail(f"required scaffold file missing: {key} -> {path}") try: path.relative_to(run_dir) except ValueError: return fail(f"scaffold file must stay inside run_dir: {key} -> {path}") if report_path != resolved["report_path"]: return fail("report path does not match manifest suggestion") if not build_report_regex().fullmatch(report_path.name): return fail(f"report filename is invalid: {report_path.name}") if report_path.suffix.lower() != str(OUTPUT["report_extension"]): return fail(f"report must use {OUTPUT['report_extension']} extension") required_sections = policy.get("required_report_sections") or OUTPUT.get("required_sections") or [] report_text = report_path.read_text(encoding="utf-8") for section in required_sections: if section not in report_text: return fail(f"report missing required section: {section}") print("OK") return 0 if __name__ == "__main__": raise SystemExit(main())
-
-
CHANGELOG.md 6 KB
# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Changed - 同步 `parallel-vibe` 默认工作区目录变更:并行评审产物路径从 `parallel_runs/.parallel-vibe/<project_id>/` 迁移到 `parallel_runs/.bensz-api/skills/parallel-vibe/<project_id>/`,更新 `build_parallel_review_plan.py`、`SKILL.md`、README、集成说明与版本号 `0.5.3 → 0.5.4`。 - 同步 `parallel-vibe` 默认工作区目录变更:并行评审产物路径从 `.parallel_vibe/<project_id>/` 改为 `.parallel-vibe/<project_id>/`,更新 `build_parallel_review_plan.py`、`SKILL.md`、README 与集成说明。 ## [0.5.3] - 2026-03-24 ### Changed - `build_parallel_review_plan.py` 改为从 `config.yaml` 解析 `parallel-vibe` 依赖脚本路径与 thread 结果 schema,并把绝对脚本路径、输入快照指纹与固定 `project_id` 写入 `parallel_review_job.json`,避免系统级安装后相对路径失效与 prompt/schema 漂移 - `build_parallel_review_plan.py` 的 `recommended_command` 现在显式带上 `--project-id`,并将 `project_id` 与输入快照一起绑定,避免证据更新后误复用旧的并行评审目录 - `parallel_plan.md` 现在会回显可直接执行的 `recommended_command`,降低二次排障时在 JSON 与 Markdown 之间来回跳转的成本 - `aggregate_parallel_reviews.py` 开始按 `config.yaml` 校验 `RESULT.md` 的必需章节、Recommendation、Risk Level、Confidence,避免把无效 thread 输出聚合成误导性共识 - `validate_review_artifacts.py` 为损坏的 `manifest.json` 补充 fail-fast 错误处理,不再抛出原始 Python traceback - `SKILL.md`、`README.md` 与 `references/parallel-vibe-integration.md` 同步更新并行依赖调用方式,明确优先使用 `parallel_review_job.json` 里的 `recommended_command`,并要求证据变化后重新生成 plan ## [0.5.2] - 2026-03-24 ### Changed - `prepare_review_job.py` 收紧输入契约:仓库输入只接受仓库根 URL 或 `owner/repo`,PR 输入只接受 PR URL、`#123`、`123` 或 `pr-123`,避免把 issue/tree 等错误链接误当成合法输入 - `build_parallel_review_plan.py` 新增 manifest 结构校验,改为从 manifest/config 读取快照目录,并对推荐命令中的路径做 shell quoting,降低配置漂移与含空格路径失败的风险 - `aggregate_parallel_reviews.py` 对缺失 project root、缺失 thread 目录、缺失 `RESULT.md` 改为明确报错,避免静默生成不完整或误导性的独立评审摘要 - `SKILL.md` / `README.md` 统一“默认优先使用内置 good-PR 标准、必要时再联网补充”的口径,并补齐 `review_count -> --n` 的参数映射说明 - `config.yaml` 版本升级为 `0.5.2`,并将 `review_count` 纳入可选输入 ## [0.5.1] - 2026-03-24 ### Changed - `SKILL.md` 不再内嵌整段最终报告模板,改为只保留必需章节清单,并统一引用 `references/report-template.md`,降低主规范噪音与重复维护成本 - `config.yaml` 的版本升级为 `0.5.1` ## [0.5.0] - 2026-03-24 ### Added - 新增 `references/good-pr-standards.md`,将“什么是好 PR”的调研结果固化为 skill 内置参考 ### Changed - `SKILL.md` 将“好 PR 标准”从“每次运行强制联网查询”调整为“默认优先使用内置参考,仅在用户明确要求或参考不足时再联网补充” - `prepare_review_job.py` 现在会把内置好 PR 标准预填充到 `notes/community_good_pr.md` ## [0.4.0] - 2026-03-24 ### Added - 新增 `references/license-checklist.md`,系统化检查依赖、vendored 代码、资源资产与许可证冲突风险 - 工作区脚手架新增 `notes/license_review.md` ### Changed - `SKILL.md` / `README.md` 将 license / 合规审查升级为标准工作流的一部分 - 最终报告模板新增 `## License / 合规审查` - 并行独立评审 thread 模板新增 `## License Review` - `config.yaml` 将 `## License / 合规审查` 加入最终报告必需章节,并将 license 风险加入独立评审关注焦点 - 版本号升级为 `0.4.0` ## [0.3.0] - 2026-03-24 ### Added - 新增基于 `parallel-vibe` 的独立并行评审能力:默认进行 5 次独立 PR review,并在最终报告中综合各线程结论 - 新增 `scripts/build_parallel_review_plan.py`:生成 `parallel-vibe` 可执行计划、输入快照与 job manifest - 新增 `scripts/aggregate_parallel_reviews.py`:聚合各 thread 的 `RESULT.md`,输出 recommendation/risk 分布与共识摘要 - 新增参考文档 `references/parallel-review-result-template.md` 与 `references/parallel-vibe-integration.md` ### Changed - `config.yaml` 新增 `parallel_review.*` 配置段,并将 `## 独立评审综合结果` 加入最终报告必需章节 - `SKILL.md` / `README.md` 升级为“默认使用 parallel-vibe 做 5 次独立评审”的工作流口径 - 版本号升级为 `0.3.0` ## [0.2.0] - 2026-03-24 ### Changed - 脚本现在从 `config.yaml` 读取目录、文件名与报告章节要求,减少配置漂移 - `prepare_review_job.py` 新增 repo/PR 一致性校验、时间戳校验与工作区占位文件创建 - `validate_review_artifacts.py` 强化默认隐藏工作区检查、脚手架文件检查与报告结构检查 - `README.md` 补齐 fail-fast 行为说明、工作区说明与 WHICHMODEL 章节 ### Added - `scripts/common_config.py`:共享配置读取逻辑 - `prepare_review_job.py` 生成的 manifest 现在记录 `skill_version` - 参考文档新增供应链、来源日期与“证据不足”口径 ## [0.1.0] - 2026-03-24 ### Added - 初始化 `git-pr-review` skill - 新增只读 PR 审查工作流,覆盖问题理解、方案分析、恶意/安全风险识别与 merge 建议 - 新增 `.git-pr-review/` 隐藏工作区约束与最终 Markdown 报告命名规则 - 新增 `prepare_review_job.py` 与 `validate_review_artifacts.py` 两个确定性脚本 - 新增报告模板、社区检索指引和安全检查清单 -
config.yaml 3.8 KB
# git-pr-review 配置文件 skill_info: name: "git-pr-review" version: "0.5.4" description: "只读审查 GitHub Pull Request:理解其问题背景、方案优劣、恶意/安全风险与是否符合社区好 PR 标准,并输出是否建议 merge 的 Markdown 报告。" author: "Bensz Conan" category: "git-security-review" directories: default_workspace: ".bensz-api/skills/git-pr-review" run_prefix: "" raw: "raw" notes: "notes" evidence: "evidence" logs: "logs" files: manifest_name: "manifest.json" raw_readme: "README.md" user_context_note: "user_context.md" community_note: "community_good_pr.md" license_review_note: "license_review.md" key_findings_note: "key_findings.md" missing_items_note: "missing_items.md" dependencies: parallel_vibe_script: "../parallel-vibe/scripts/parallel_vibe.py" output: report_prefix: "Git-PR-Review" report_extension: ".md" default_report_dir: "." timestamp_format: "%Y-%m-%d-%H-%M" enforce_hidden_workspace_when_default: true required_sections: - "## 结论摘要" - "## 独立评审综合结果" - "## PR 在解决什么问题" - "## 方案分析" - "## 恶意/安全风险审查" - "## License / 合规审查" - "## 与“好 PR”社区标准的对照" - "## 关键证据" - "## 证据不足与待确认点" - "## 建议的处理方式" parallel_review: enabled: true default_review_count: 5 min_review_count: 1 max_review_count: 9 thread_id_width: 3 workspace_dir: "parallel_review" input_snapshot_dir: "input_snapshot" parallel_out_dir: "parallel_runs" plan_filename: "parallel_plan.json" plan_markdown: "parallel_plan.md" job_manifest_name: "parallel_review_job.json" aggregate_markdown: "independent_review_summary.md" aggregate_json: "independent_review_summary.json" result_filename: "RESULT.md" confidence_levels: - "High" - "Medium" - "Low" required_result_sections: - "# Independent PR Review" - "## Final Call" - "## Problem Understanding" - "## Strengths" - "## Limitations" - "## Security Review" - "## License Review" - "## Good PR Criteria Comparison" - "## Key Evidence" - "## Evidence Gaps" - "## Suggested Disposition" review_runner: type: "claude" profile: "deep" synthesis: enabled: false runner: type: "claude" profile: "deep" review_focuses: - "整体问题理解与 merge 决策" - "恶意/安全风险与供应链风险" - "许可证兼容性、copyleft 风险与第三方归属" - "可维护性、回滚性与作用域漂移" - "测试、验证证据与 CI 信号" - "社区好 PR 标准与 reviewer 体验" inputs: required: - github_repo - github_pr optional: - extra_instructions - review_count - workspace_dir - report_dir review_policy: review_mode: "read_only" bundled_good_pr_reference_required: true online_refresh_for_good_pr: "explicit_or_insufficient" min_external_sources: 2 min_official_or_maintainer_sources: 1 final_recommendations: - "Merge" - "Merge after fixes" - "Request changes" - "Do not merge" - "Escalate security review" risk_levels: - "Low" - "Medium" - "High" - "Critical" security_checks: categories: - "credential_exposure" - "remote_execution_or_download" - "permission_or_ci_change" - "data_destruction_or_disable_guardrail" - "obfuscation_or_hidden_payload" - "scope_drift_or_social_engineering" read_only_sources: preferred: - "GitHub web pages" - "GitHub API / .diff / .patch" - "gh pr view" - "gh pr diff" - "gh api" forbidden: - "gh pr checkout" - "execute PR branch code" - "install PR-introduced dependencies" - "merge / approve / rebase / squash" -
README.md 12.4 KB
# git-pr-review 本 README 面向**使用者**:如何触发并正确使用 `git-pr-review` skill。 执行规范在 `SKILL.md`;默认参数与命名规则在 `config.yaml`。 ## 用法 推荐用法: ```text 请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并判断是否值得 merge。 输入:仓库地址 `https://github.com/owner/repo`,PR `https://github.com/owner/repo/pull/123` 输出:项目根目录下 1 份 Markdown 审查报告 ``` 进阶用法: ```text 请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并重点检查是否存在恶意代码或供应链风险。 输入:仓库地址 `https://github.com/owner/repo`,PR `#123`,另外参考我附上的背景说明 输出:项目根目录下 1 份 Markdown 审查报告;所有中间文件放到 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` ``` 并行独立评审版: ```text 请使用 git-pr-review skill 帮我 review 这个 GitHub PR,并做 5 次独立评审后再综合结论。 输入:仓库地址 `https://github.com/owner/repo`,PR `#123` 输出:项目根目录下 1 份最终 Markdown 审查报告;`.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` 里保留并行独立评审的全部中间产物 ``` ## 它能帮你做什么 `git-pr-review` 适合下面这些场景: - 你想知道某个 PR 到底解决了什么问题 - 你不确定这个 PR 的方案值不值得 merge - 你担心 PR 里藏着恶意逻辑、安全问题、权限扩大或 CI/CD 风险 - 你担心 PR 引入新的依赖、第三方代码或资源后会带来 license 冲突 - 你希望把这个 PR 和社区对“好 PR”的标准做一次对照 - 你希望让多个独立 reviewer 视角并行审查,再综合得出更稳的判断 - 你想把结论沉淀成一份可转发、可归档的 Markdown 报告 ## 工作方式 这个 skill 默认是**只读评审**: - 会读取 GitHub 上的 PR 信息、diff、评论、CI 状态和关联 issue - 默认优先使用 skill 内置的“好 PR”标准参考,而不是每次实时联网 - 只有用户明确要求最新口径、指定特定社区规范,或内置标准明显不足时,才会联网补充“好 PR”标准来源 - 会默认基于 `parallel-vibe` 做 **5 次独立评审** - 会在涉及依赖、vendored 代码、复制资源时给出 license / 合规建议 - 会把所有中间文件放进工作目录下的 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` - 会在项目根目录生成最终报告 它**不会**: - 修改仓库代码 - 自动 merge PR - approve PR - checkout PR 分支后运行不可信代码 ## 输出文件 默认会生成: - `Git-PR-Review_{repo}_{pr}_{yyyy-mm-dd-hh-mm}.md` 例如: - `Git-PR-Review_openai_openai-python_pr-2451_2026-03-24-15-30.md` 中间文件默认位于: - `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/` 其中通常包含: - `manifest.json`:本次评审的路径和元数据 - `raw/README.md`:原始材料目录说明 - `notes/user_context.md`:用户补充背景 - `notes/community_good_pr.md`:社区标准摘记 - `references/good-pr-standards.md`:skill 内置的“好 PR”标准参考 - `notes/license_review.md`:license / 合规审查笔记(如适用) - `evidence/key_findings.md`:关键发现 - `evidence/missing_items.md`:缺失证据与影响 - `parallel_review/parallel_plan.json`:并行独立评审计划 - `parallel_review/parallel_runs/.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/parallel-vibe/<project_id>/...`:独立 reviewer threads 产物 - `parallel_review/independent_review_summary.md`:独立评审聚合结果 ## 关键行为 ### Fail-fast 校验 脚本会尽早拦截明显错误,避免你在错误上下文上浪费时间: - 仓库地址不是 GitHub 仓库,或误传成 `issues/`、`pull/`、`tree/` 这类子页面 URL - PR URL 不是 GitHub Pull Request,或编号格式不是 `#123` / `123` / `pr-123` - 仓库与 PR URL 不属于同一个仓库 - 最终报告文件名或章节结构不符合约定 ### 工作区隔离 - 默认中间文件只允许写入 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` - 最终 Markdown 报告默认写到当前工作目录 - 如果你明确指定其他 `workspace_dir` 或 `report_dir`,skill 会按你的指定执行 ### 默认独立评审次数 - 默认会做 `5` 次独立评审 - 如果你明确指定 `review_count=7` 或“做 7 次独立评审”,skill 会按你的值执行 - 在底层辅助脚本里,这个值会映射为 `build_parallel_review_plan.py --n <review_count>` - 如果你只想单次评审,也可以明确要求 `review_count=1` ## 使用示例 ### 示例 1:常规技术评审 ```text 请使用 git-pr-review skill 帮我 review 这个 PR。 输入:仓库 `https://github.com/pallets/flask`,PR `https://github.com/pallets/flask/pull/5432` 输出:给我一份是否建议 merge 的 Markdown 报告 ``` ### 示例 2:重点怀疑恶意行为 ```text 请使用 git-pr-review skill 评估这个 PR 是否恶意。 输入:仓库 `https://github.com/owner/repo`,PR `#87` 输出:一份带风险等级和处理建议的报告 ``` ### 示例 3:带团队背景说明 ```text 请使用 git-pr-review skill 审查这个 PR。 输入:仓库 `https://github.com/owner/repo`,PR `#201` 另外,还有下列参数约束: - 重点看 CI/CD、权限配置和 secrets 是否被动了手脚 - 我们团队更偏好小而清晰、易回滚的 PR - 输出时请明确写出“是否建议 merge” ``` ### 示例 4:指定独立评审次数 ```text 请使用 git-pr-review skill 审查这个 PR,并做 7 次独立评审。 输入:仓库 `https://github.com/owner/repo`,PR `#88` 输出:最终审查报告中请单独总结 7 次独立评审的共识与分歧 ``` ## 结果里会回答哪些问题 最终报告通常会明确回答: - 多个独立 reviewer 的 recommendation / risk 是否形成共识 - 这个 PR 解决的问题是否真实、是否描述清楚 - 方案的优势、局限和潜在替代路径 - 改动范围是否合理,是否便于 review 和回滚 - 是否存在恶意或高风险信号 - 是否存在 license 冲突、copyleft 风险、第三方归属缺失或需要法务确认的点 - 它是否符合社区里“好 PR”的基本标准 - 如果证据不够,缺的是什么、会如何影响结论 - 最后到底建议 `Merge`、`Request changes` 还是 `Do not merge` ## 常见问题 ### Q:它会不会自己把 PR merge 掉? 不会。除非你明确提出,否则这个 skill 不会执行 merge、approve、rebase、squash 这类操作。 ### Q:它会不会运行 PR 里的代码? 默认不会。这个 skill 的原则是只读评审,避免执行不可信代码。 ### Q:如果我想把中间文件放到别处呢? 可以。默认目录是 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/`,但如果你明确指定其他目录,skill 会按你的目录来。 ### Q:它会只看 diff 吗? 不会。它还会结合 PR 描述、评论、CI 状态、关联 issue,以及外部社区对“好 PR”的标准一起判断。 ### Q:它会检查 license 问题吗? 会。尤其当 PR 引入新依赖、复制第三方代码、字体、图标、模板、数据或模型资源时,它会明确给出 license / 合规风险和建议动作。 ### Q:默认为什么是 5 次独立评审? 因为 5 个独立视角通常足够覆盖:整体质量、安全风险、可维护性、测试/验证、社区标准这几类核心问题,同时又不会把成本拉得过高。 ## 备选用法(脚本/硬编码流程) ### 步骤 1:创建隔离工作区 ```bash python3 git-pr-review/scripts/prepare_review_job.py \ --repo "https://github.com/owner/repo" \ --pr "https://github.com/owner/repo/pull/123" ``` ### 步骤 2:完成评审后校验产物 ```bash python3 git-pr-review/scripts/validate_review_artifacts.py \ --manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \ --report /absolute/path/to/Git-PR-Review_<...>.md ``` ### 步骤 3:生成 parallel-vibe 并行独立评审计划 ```bash # 例如用户要求 review_count=5 时,这里传 --n 5 python3 git-pr-review/scripts/build_parallel_review_plan.py \ --manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \ --n 5 ``` ### 步骤 4:运行 parallel-vibe 独立评审 ```bash # 推荐直接复制 `parallel_review_job.json` 里的 `recommended_command` python3 ../parallel-vibe/scripts/parallel_vibe.py \ --plan-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_plan.json \ --src-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/input_snapshot \ --out-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_runs \ --project-id <parallel_review_job.json.project_id> ``` 补充说明: - `build_parallel_review_plan.py` 会把解析后的 `parallel-vibe` 脚本路径、固定 `project_id` 和可直接执行的 `recommended_command` 写进 `parallel_review/parallel_review_job.json` - 如果你补充了 `raw/`、`notes/`、`evidence/` 里的材料,请重新运行 `build_parallel_review_plan.py`,让输入快照和 `project_id` 一起刷新 ### 步骤 5:聚合独立评审结果 ```bash python3 git-pr-review/scripts/aggregate_parallel_reviews.py \ --job-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_review_job.json ``` 如果 `parallel-vibe` 尚未执行完成,或某个 thread 没有生成 `RESULT.md`,聚合脚本会直接报错并提示先补齐输入,避免拿不完整结果做 merge 决策。 ## 更多文档 - `SKILL.md`:执行流程与硬规则 - `config.yaml`:版本、命名与审查口径 - `references/report-template.md`:报告模板 - `references/good-pr-standards.md`:内置“好 PR”标准参考 - `references/security-checklist.md`:恶意/安全审查清单 - `references/license-checklist.md`:license / 合规审查清单 - `references/community-research-playbook.md`:社区标准检索建议 - `references/parallel-review-result-template.md`:独立评审 thread 模板 - `references/parallel-vibe-integration.md`:parallel-vibe 集成说明 ## WHICHMODEL - 模型选择最佳实践 **最后更新**:2026-03-24 ### 披露信息 - **覆盖厂商**:Anthropic、OpenAI - **来源类型**:官方模型文档 / 官方模型总览 - **局限性**:本节优先采用官方资料来保证时效与权威性,未纳入社区主观体验对比;因此更适合做“保守默认选择”,不适合做极端成本优化结论 ### 场景 1:默认 PR 审查 | 项目 | 建议 | |------|------| | 推荐模型 | Claude Sonnet 4 / GPT-5.2 | | 推荐原因 | 这个 skill 需要同时处理 diff、评论、社区资料和安全判断,属于“中高复杂度的多源证据综合”;默认优先选择推理和速度更平衡的模型 | | 适用情况 | 常规 PR、几十到上百行 diff、需要联网和结构化报告输出 | ### 场景 2:高风险 / 安全敏感 PR | 项目 | 建议 | |------|------| | 推荐模型 | Claude Opus 4.1 / GPT-5.2 | | 推荐原因 | 当 PR 涉及 CI/CD、权限、密钥、供应链或大规模重构时,更强的上下文整合和细粒度推理更重要 | | 适用情况 | 可疑 PR、超大 diff、需要更谨慎的恶意行为识别 | ### 场景 3:快速分诊 | 项目 | 建议 | |------|------| | 推荐模型 | Claude Haiku 3.5 / GPT-5 mini | | 推荐原因 | 仅做“先看值不值得深审”的低成本分诊时,可以用更便宜更快的模型先产出初筛结论 | | 适用情况 | 批量 triage、多 PR 排队、先判断是否需要人工深审 | ### 选择原则 1. 只做初筛时优先“快而便宜”;一旦涉及安全与 merge 决策,就升级到默认档或高风险档。 2. 这个 skill 的核心不是写代码,而是**跨来源证据综合判断**,所以默认更适合通用高推理模型,而不是纯编码特化模型。 3. 如果你打算把这个 skill 嵌入会频繁读取本地仓库、做更多 agentic coding 操作的工作流里,再考虑编码特化模型;单次 PR 决策报告通常不需要这样做。 ### 官方参考 - Anthropic Models Overview: https://docs.anthropic.com/en/docs/about-claude/models/overview - OpenAI Models Overview: https://platform.openai.com/docs/models -
SKILL.md 15.7 KB
--- name: git-pr-review description: 当用户明确要求审查 GitHub PR、评估是否合并或判断 PR 应如何处理时使用。对 PR 做只读、证据驱动的方案与风险评估,并给出合并建议。⚠️ 不适用:直接修改、合并 PR,或在本地执行不可信 PR 代码。 metadata: author: Bensz Conan short-description: 只读审查 GitHub Pull Request,并产出可决策的 Markdown 报告 keywords: - git-pr-review - GitHub PR review - pull request review - PR 安全审查 - merge decision --- # Git PR Review ## 目标 当用户明确要求“review 某个 GitHub PR”“评估某个 pull request 是否值得 merge”“帮我判断这个 PR 怎么处理”时使用。基于用户提供的 GitHub 仓库地址、PR 编号/链接和补充说明,进行只读、证据驱动的 PR 审查:理解 PR 解决的问题、评估方案优劣与局限、默认优先使用内置“好 PR”标准并在必要时联网补充、识别恶意或高风险改动,并输出是否建议 merge 的 Markdown 决策报告。⚠️ 不适用:用户要你直接修改 PR 代码、直接 merge PR、或在本地执行 PR 分支中的不可信代码。 ## 流程 ### 输入 #### 你需要确认的输入 1. `github_repo`(必需) - GitHub 仓库根地址或 `owner/repo`,例如 `https://github.com/owner/repo` - 不接受 `issues/`、`pull/`、`tree/` 这类子页面 URL 2. `github_pr`(必需) - PR URL、`#123`、`123` 或 `pr-123` 3. `extra_instructions`(可选) - 用户已有判断、关注点、禁区、参考材料、团队背景 4. `review_count`(可选) - 独立评审次数,默认 5;用户明确指定时以用户要求为准 - 当你调用 `build_parallel_review_plan.py` 时,把它映射为 `--n <review_count>` 5. `workspace_dir`(可选) - 默认 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` 6. `report_dir`(可选) - 默认当前工作目录(项目根) ### 执行步骤 #### 核心原则 - **只读优先**:默认只能读取 GitHub 页面、API 返回、diff、评论、CI 状态、相关 issue/文档;不要修改源代码。 - **绝不主动 merge**:除非用户明确要求,否则不要执行 merge、rebase、squash、approve、request review 等操作。 - **绝不执行不可信 PR 代码**:不要 `gh pr checkout`、不要运行 PR 分支脚本、不要安装 PR 引入的依赖、不要触发可疑 CI/CD。 - **中间文件隔离**:所有中间文件必须保存在工作目录下的隐藏目录 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/`;若用户另有指定,才使用用户指定目录。 - **证据驱动**:所有结论都要回到证据,明确引用 diff、评论、CI、issue、社区资料或官方文档。 - **合规不忽略**:如果 PR 触及依赖、vendored 代码、复制粘贴第三方内容、资源资产或许可证文件,必须显式审查 license 风险与兼容性。 #### 标准工作流 ##### 1. 初始化隔离工作区 优先使用确定性脚本创建本次评审目录与建议输出文件名: ```bash python3 git-pr-review/scripts/prepare_review_job.py \ --repo "https://github.com/owner/repo" \ --pr "https://github.com/owner/repo/pull/123" ``` 脚本会: - 校验仓库地址与 PR URL 是否属于同一 GitHub 仓库 - 创建本次运行目录(默认 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/`) - 生成 `manifest.json` - 预创建最小占位文件: - `raw/README.md` - `notes/user_context.md` - `notes/community_good_pr.md` - `evidence/key_findings.md` - `evidence/missing_items.md` - 输出建议的最终报告路径(默认项目根) 从这一步开始,所有抓取到的原始材料、分析笔记、临时摘要、命令输出都写入该工作区。 ##### 2. 只读获取 PR 证据 优先选择**不会修改仓库状态**的方式读取 PR: - GitHub 网页 / API / `.patch` / `.diff` - `gh pr view`、`gh pr diff`、`gh api` 这类只读命令 - 仓库内公开 issue、discussion、相关文档、CI 状态页 建议至少保存下列材料到工作区: - `raw/pr_meta.*`:标题、作者、状态、标签、基线分支、merge 状态、CI 状态 - `raw/pr_diff.*`:完整 diff 或 patch - `raw/pr_comments.*`:review comments / discussion(如有) - `raw/linked_issues.*`:关联 issue / discussion / docs(如有) - `notes/user_context.md`:用户给的补充信息 - `notes/community_good_pr.md`:关于“好 PR”的社区标准摘记与链接 - `notes/license_review.md`:license / 合规审查笔记(如本次 PR 相关) - `evidence/missing_items.md`:缺失材料、原因与影响 如果某项拿不到,要在工作区里记录“未获取原因”以及它对结论的影响,而不是静默跳过。 ##### 3. 基于 parallel-vibe 做 N 次独立评审(默认 5 次) 目标: - 使用 `parallel-vibe` 在多个独立 workspace 中做 **N 次彼此独立的 PR 审查** - 默认 `N=5` - 每个 thread 必须独立产出 `RESULT.md` - 最后把多份 `RESULT.md` 聚合为一份统一摘要,再用于最终报告 推荐顺序: ```bash python3 git-pr-review/scripts/build_parallel_review_plan.py \ --manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \ --n 5 ``` 这里的 `--n` 是 helper script 参数;如果用户说“做 7 次独立评审”,等价于 `review_count=7`,最终在脚本层落成 `--n 7`。 该脚本会在本次 run 目录下生成: - `parallel_review/input_snapshot/`:供每个 thread 复制的审查输入快照 - `parallel_review/parallel_plan.json`:`parallel-vibe` 的计划文件 - `parallel_review/parallel_review_job.json`:后续运行与聚合所需路径、已解析好的 `parallel-vibe` 脚本路径、固定的 `project_id` 与 `recommended_command` 重要说明: - 优先直接执行 `parallel_review/parallel_review_job.json` 里的 `recommended_command`,避免手动拼路径或 `project_id` - 如果 `raw/`、`notes/`、`evidence/` 有变化,必须重新运行 `build_parallel_review_plan.py`,让输入快照和 `project_id` 一起刷新 然后运行: ```bash # 更推荐直接复制 `parallel_review_job.json` 里的 `recommended_command` python3 ../parallel-vibe/scripts/parallel_vibe.py \ --plan-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_plan.json \ --src-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/input_snapshot \ --out-dir .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_runs \ --project-id <parallel_review_job.json.project_id> ``` 并行独立评审完成后,聚合结果: ```bash python3 git-pr-review/scripts/aggregate_parallel_reviews.py \ --job-file .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/parallel_review/parallel_review_job.json ``` 如果 `parallel-vibe` 还没跑完、某个 thread 缺少 `RESULT.md`,或聚合输入不完整,聚合脚本应明确报错并停止,而不是静默生成误导性摘要。 聚合脚本会生成: - `parallel_review/independent_review_summary.md` - `parallel_review/independent_review_summary.json` 要求: - 每个 thread 都要基于当前 workspace 中的材料独立审查,不看其他 thread 结果 - 每个 thread 的 `RESULT.md` 必须至少包含 recommendation / risk / evidence gaps - 最终主报告必须明确综合独立评审的共识与分歧 ##### 4. 理解这个 PR 在解决什么 至少回答清楚: - PR 试图解决的核心问题是什么 - 这个问题是否真实存在,证据来自哪里 - PR 采用了什么方案 - 方案的主要优点、局限和潜在替代方案 - PR 是否与标题、描述、commit、测试、文档保持一致 如果 PR 描述模糊,必须从 diff、关联 issue、评论线程中补足上下文。 ##### 5. 恶意 / 高风险 PR 审查 必须显式判断该 PR 是否存在恶意或高风险特征,并给出风险等级: `Low` / `Medium` / `High` / `Critical` 重点检查: - 是否存在数据窃取、凭证泄露、遥测偷报、后门逻辑 - 是否引入可疑下载、远程执行、shell 注入、SQL 注入、权限提升、破坏性删除 - 是否修改 CI/CD、部署脚本、权限配置、密钥读取路径、发布流程 - 是否引入难以解释的混淆代码、base64/hex 载荷、动态执行链 - 是否借“重构/清理”名义绕开测试、放宽校验、关闭安全保护 - 是否故意减少日志、删除告警、掩盖审计痕迹 - 是否存在明显不符合项目目标的改动范围漂移 只要怀疑恶意,就要把建议提升为: - `不要 merge` - `建议人工安全复核 / 维护者升级处理` ##### 6. License / 合规审查 这是强制步骤之一。即使用户没有主动提到 license,只要 PR 涉及下列内容,就必须显式检查: - 新依赖、依赖升级、包管理器锁文件 - vendored 代码、复制的第三方脚本/模板/字体/图标/数据 - `LICENSE` / `NOTICE` / copyright header - 模型权重、数据集、示例代码、前端资源 至少回答清楚: - PR 是否引入了新的 license 风险或兼容性风险 - 是否存在互相冲突的 license / header / 声明 - 是否需要更新 `LICENSE`、`NOTICE`、README、第三方声明文档 - 该风险是否需要法务/维护者额外确认 对于 license 不明确、明显冲突、或可能触发 GPL/AGPL/SSPL 等传播义务的情况: - 不能直接给出乐观 merge 建议 - 应提升为 `Request changes`、`Do not merge` 或至少 `Escalate security review` / 合规复核 参考:`references/license-checklist.md` ##### 7. 使用内置“好 PR”标准(默认不实时联网) 这里的默认做法**不是每次执行都实时联网**。 本 skill 已经把“什么是好 PR”的基础标准沉淀到: - `references/good-pr-standards.md` 并且在初始化工作区时,会自动把该参考摘要写入: - `notes/community_good_pr.md` 默认执行时: - 优先使用 `notes/community_good_pr.md` 里的内置标准 - 结合当前 PR 做对照分析 - 在最终报告中明确写出“哪些维度符合 / 不符合” 只有在以下情况才需要再联网补充: - 用户**明确要求**查看最新社区口径 - 用户指定某个特定社区 / 组织 / 仓库的 PR 规范 - 当前内置标准不足以覆盖该 PR 的特殊场景 即使需要补充联网,也应把新增来源继续沉淀到本次 run 的 `notes/community_good_pr.md`,而不是只在脑中使用。 不是机械照抄“最佳实践”,而是要回答: - 这个 PR 在哪些维度上符合好 PR 的定义 - 哪些维度不符合 - 不符合之处是“小瑕疵”还是“阻断 merge 的问题” ##### 8. 输出决策报告 将最终结论写成 Markdown,默认放在项目根目录,文件名格式: `Git-PR-Review_{repo_slug}_{pr_slug}_{timestamp}.md` 例如: `Git-PR-Review_openai_openai-python_pr-2451_20260324153022.md` 报告必须包含以下章节: - `## 结论摘要` - `## 独立评审综合结果` - `## PR 在解决什么问题` - `## 方案分析` - `## 恶意/安全风险审查` - `## License / 合规审查` - `## 与“好 PR”社区标准的对照` - `## 关键证据` - `## 证据不足与待确认点` - `## 建议的处理方式` 完整模板不要直接内嵌在 `SKILL.md`,而是统一引用: - `references/report-template.md` 写最终报告时,必须综合: - 原始证据(`raw/`、`notes/`、`evidence/`) - `parallel_review/independent_review_summary.md` - 如有必要,`parallel_review/parallel_runs/.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/parallel-vibe/<project_id>/@main/summary.md` ##### 9. 完成前自检 在交付前运行校验脚本,确认: - 工作区结构完整 - 默认模式下工作区仍位于隐藏目录 - 最终报告文件名合规 - 最终报告包含配置要求的所有必需章节 - 没有把中间产物散落到工作区外 ```bash python3 git-pr-review/scripts/validate_review_artifacts.py \ --manifest .bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/{yyyy-mm-dd-hh-mm}/manifest.json \ --report /abs/path/to/Git-PR-Review_<...>.md ``` #### 可读参考 - `references/report-template.md`:最终报告模板 - `references/security-checklist.md`:恶意/高风险 PR 审查清单 - `references/license-checklist.md`:license / 合规审查清单 - `references/community-research-playbook.md`:如何搜索“好 PR”社区标准 - `references/parallel-review-result-template.md`:独立评审 thread 的 `RESULT.md` 结构 - `references/parallel-vibe-integration.md`:`parallel-vibe` 集成方式与关键产物 ### 输出 #### 输出要求 - 最终交付物:1 份 Markdown 报告 - 报告用语要明确,不要模棱两可 - 结论必须给出处理建议,不要只给“分析但不表态” - 如果证据不足,要明确说明不足项,以及这如何影响你的建议 ### 输出管理 #### BenszAPI 任务工作区 ### 校验 交付前运行 `validate_review_artifacts.py`,确认 manifest、原始证据、独立评审汇总和最终报告齐全,报告包含配置要求的全部章节、建议明确且所有结论可回到证据;核对工作区仍在 `.bensz-api` 隐藏目录且未执行 PR 代码或远程写操作。 ### 失败与恢复 仓库/PR 证据获取失败、并行 thread 缺少 `RESULT.md`、聚合输入不完整或报告校验失败时,保留已获取材料与错误摘要,停止生成乐观结论并明确证据缺口;不得通过 checkout、运行不可信代码或修改远程状态来绕过阻塞。 ## 约束 <!-- BEGIN COMMON CONSTRAINTS --> <!-- Source-Hash: sha256:15120201e9e0c7569517261d57ecefb63ac279c26ed13876f8e95b6dc35854d3 --> <!-- Template-ID: skill-common-constraints; Template-Version: 1; Sync-Policy: exact-block --> ### 公共硬约束 本块由 `docs/templates/skill-common-constraints.md` 统一维护;每个 `SKILL.md` 的 `## 约束` 必须逐字同步本块,不得在副本中改写公共规则。 - 任务需要落盘时,使用唯一的 `./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/` 根目录;共享材料放入 `shared/`,Skill 专属材料放入该 Skill 的 `input/`、`output/`、`log/`。 - 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。 - 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。 - 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。 - 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。 - Skill 版本唯一记录在自身 `config.yaml:skill_info.version`;公开 API、协议、目录或配置变更同步文档与 `CHANGELOG.md`。 - `bensz-collect-bugs` 是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入 `~/.bensz-skills/bugs/`,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。 <!-- End of canonical common constraints. --> <!-- END COMMON CONSTRAINTS --> ### Skill 专属约束 #### 明确禁止事项 - 不要修改目标仓库代码 - 不要 merge PR - 不要 approve PR - 不要 checkout PR 分支并执行代码 - 不要把 API token、cookie、认证信息写入工作区 - 不要把中间文件写到 `.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/git-pr-review/` 之外(除最终 Markdown 报告)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.