Claude Skill

git-pr-review

当用户明确要求“review 某个 GitHub PR”“评估某个 pull request 是否值得 merge”“帮我判断这个 PR 怎么处理”时使用。基于用户提供的 GitHub 仓库地址、PR 编号/链接和补充说明,进行只读、证据驱动的 PR 审查:理解 PR 解决的问题、评估方案优劣与局限、默认优先使用内置“好 PR”标准并在必要时联网补充、识别恶意或高风险改动,并输出是否建议 merge 的 Markdown 决策报告。⚠️ 不适用:用户要你直接修改 PR 代码、直接 merge PR、或在本地执行 PR 分支中的不可信代码。

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download huangwb8-skills-skills_alpha_git-pr-review-dd1fab8.zip · 39 KB
Part of huangwb8/skills — 22 skills

Install

skills CLI npx skills add https://github.com/huangwb8/skills/tree/main/skills/alpha/git-pr-review
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install huangwb8-skills@llmmart
Git 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 排队、先判断是否需要人工深审

选择原则

  1. 只做初筛时优先“快而便宜”;一旦涉及安全与 merge 决策,就升级到默认档或高风险档。
  2. 这个 skill 的核心不是写代码,而是跨来源证据综合判断,所以默认更适合通用高推理模型,而不是纯编码特化模型。
  3. 如果你打算把这个 skill 嵌入会频繁读取本地仓库、做更多 agentic coding 操作的工作流里,再考虑编码特化模型;单次 PR 决策报告通常不需要这样做。

官方参考

Skill manifest

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. 初始化隔离工作区

优先使用确定性脚本创建本次评审目录与建议输出文件名:

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 聚合为一份统一摘要,再用于最终报告

推荐顺序:

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.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. 完成前自检

在交付前运行校验脚本,确认:

  • 工作区结构完整
  • 默认模式下工作区仍位于隐藏目录
  • 最终报告文件名合规
  • 最终报告包含配置要求的所有必需章节
  • 没有把中间产物散落到工作区外
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.

No comments yet.

Reviews (0)

No reviews yet.

Related