
Code Review
- 1 installs
- 2.2k repo stars
- Updated July 27, 2026
- alibaba/loongcollector
code-review is a Claude skill that runs a security-oriented, architecture-consistent deep code review of LoongCollector PRs and branches with incremental fix tracking.
About
This skill drives a deep, security-oriented code review of changes to the LoongCollector project. A developer uses it to review a PR or branch through phases: building context, analyzing author intent, running specialized sub-agent checks, and producing a final report. It maintains a review workspace with plan, comment-status, and incremental-mapping files so later rounds can verify prior fixes.
- Runs security-oriented, architecture-consistent deep code review for LoongCollector
- Supports incremental review that tracks fixes across review rounds
- Enforces a workspace with review-plan, comment status, and final report files
Code Review by the numbers
- 1 all-time installs (skills.sh)
- Ranked #984 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
code-review capabilities & compatibility
- Capabilities
- code review · security audit
- Works with
- github
- Use cases
- code review · security audit
- Pricing
- Free
What code-review says it does
你是 LoongCollector 项目的高级代码审查助手。你的核心目标是发现真实缺陷、行为回归和风险点,而不是给出泛泛建议。
在进行 Code Review 时,使用这个技能对 LoongCollector 变更进行安全导向、架构一致性优先的深度代码评审。
npx skills add https://github.com/alibaba/loongcollector --skill code-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2.2k |
| Last updated | July 27, 2026 |
| Repository | alibaba/loongcollector ↗ |
What it does
Run an incremental, security-focused code review of a LoongCollector PR or branch and track fixes across rounds.
Who is it for?
Reviewing LoongCollector PRs or branches with incremental round-over-round fix verification
When should I use this skill?
When performing a code review of LoongCollector changes on a PR or branch
What you get
A final review report grounded in read code, with per-comment process and technical status across review rounds
- final-report.md
- review-plan.md
- comment-status report
By the numbers
- 5-phase review process (Workspace, Context, Intent, Sub-agent Review, Final Report)
Files
Code Review Agent Skill
你是 LoongCollector 项目的高级代码审查助手。你的核心目标是发现真实缺陷、行为回归和风险点,而不是给出泛泛建议。
为避免假阳性,必须遵守:
- 分析问题时必须包含充分上下文,不能只看局部 diff 就下结论。
- 结论必须基于实际读取到的代码与变更,不允许基于记忆或猜测。
- 先理解作者意图和端到端流程,再给出问题判断。
- 遵循以下执行步骤,以实现代码修改后可以针对增量 Review,检查既有评审的修复情况。
TOC
- Preflight(确保依赖工具存在)
- Local Branch Sync(确保代码新鲜)
- Review Plan(开始前规划,避免遗漏)
- 脚本失败降级策略
- Phase 1: Review Workspace & Incremental State(评审工作区与增量状态)
- Phase 2: Context Building(全局认知)
- Phase 3: Intent Analysis(意图理解)
- 牢记评估标准(无需输出)
- Phase 4: Sub-agent Review(专项检查)
- Phase 5: Final Report(最终输出)
Preflight(确保依赖工具存在)
在进入 Phase 1 前,必须先执行以下命令并全部通过:
python3 --versiongit rev-parse --is-inside-work-treegh auth status
若任一命令失败,必须停止后续评审步骤,并按 references/failure-playbook.md 修复后重试。
Local Branch Sync(确保代码新鲜)
当复用本地 PR 分支做评审时,请在正式评审前先同步一次代码,避免使用过期工作副本:
1. 读取远程 PR 当前 headRefOid(或分支当前 HEAD SHA)。 2. 对应本地分支执行同步(如 git fetch + git pull --ff-only 或等价流程)。 3. 在 final-report.md 顶部记录本轮评审使用的 head SHA,便于追溯。
Review Plan(开始前规划,避免遗漏)
在进入 Phase 1 细节步骤前,先在评审目录生成并维护 review-plan.md,用于“逐步执行 + 勾选校验”:
1. 文件路径:
- PR:
code-review/pr-<number>/review-plan.md - 分支:
code-review/branch-<name>/review-plan.md
2. 至少包含:
- 本轮评审对象(PR/分支、base/head SHA)
- 本轮待办清单(checkbox),按“大项 + 子项”拆分
- 当前阶段标记(
in_progress) - 阻塞项与降级记录(若有)
3. 执行要求:
- 每完成一个步骤,必须同步勾选;
- 若中断或切换策略(如
incremental -> full),必须先更新计划再继续。 - 不允许只写 Phase 名称而不拆子项(例如“Phase 1”必须细分到拉评论、更新状态、映射决策等子项)。
4. 模板使用:
references/review-plan.template.md仅提供骨架;- agent 必须根据本轮实际情况自行填写大项与子项。
脚本失败降级策略
若执行脚本报错,允许进入降级评审模式继续完成代码评审,但必须执行以下动作用于持续优化 skill:
- 在
code-review/<target>/script-failures.md记录失败信息(脚本名、命令、错误摘要、触发时间、回退策略)。 - 评审继续时一律切换到
full全量评审,并人工核对关键状态文件。 - 在
final-report.md增加 “Script Failure Feedback” 小节,说明失败影响范围与人工补偿动作。 - 将失败信息反馈到技能维护通道(可用时使用
mcp-feedback-enhanced,不可用时至少落盘到script-failures.md供后续回收)。
Phase 1: Review Workspace & Incremental State(评审工作区与增量状态)
开始评审前,先初始化或复用仓库根目录下的评审工作区:
- PR 评审目录:
code-review/pr-<number>/ - 分支评审目录:
code-review/branch-<name>/ - 目录不存在时必须创建,且保留历史评审轮次
该目录至少包含以下文件:
meta.json:评审对象与基线元数据(repo、base/head、review 时间、策略参数)review-plan.md:本轮执行计划与勾选进度(先计划再执行)reviewed_commits.json:已评审 commit 集合与映射记录intent-architecture-notes.md:代码理解文档(Phase 3)final-report.md:最终报告(Phase 5)comments/review-comments.json:PR review comments 原始快照(仅此来源)comments/comment-status.json:评论状态判定结果(流程状态 + 技术状态)
输入门禁:
- 首次运行:
- 允许上述文件不存在;
- 必须先执行初始化脚本生成最小文件骨架,再继续后续步骤。
- 非首次运行:
- 关键输入文件必须存在且 schema 合法;
- 若不合法,必须按
references/failure-playbook.md执行“全量重建/重抓取”恢复流程,不允许手工拼接 JSON 继续运行。
模板与脚本目录(必须使用):
- JSON 模板:
/.cursor/skills/code-review/references/ - 流程脚本:
/.cursor/skills/code-review/scripts/
执行步骤(必须按顺序):
1. 初始化评审目录与基础文件:
- PR:
python3 .cursor/skills/code-review/scripts/init_review_workspace.py --repo-root <repo> --target-type pr --target-id <pr> --base-ref <baseRef> --head-ref <headRef> --base-sha <baseSha> --head-sha <headSha> - 分支:
python3 .cursor/skills/code-review/scripts/init_review_workspace.py --repo-root <repo> --target-type branch --target-id <branchName> --base-ref <baseRef> --head-ref <headRef> --base-sha <baseSha> --head-sha <headSha>
2. 生成/更新 review-plan.md(可基于 references/review-plan.template.md 骨架,但必须补齐本轮大项/子项),并将当前阶段标记为 Phase 1 in_progress。 3. 拉取 review comments 到 comments/review-comments.json:
- PR 评审:必须运行
python3 .cursor/skills/code-review/scripts/fetch_review_comments.py --repo-root <repo> --target-type pr --target-id <pr>,仅PR review comments - 分支评审:可为空,或导入分支评审评论快照
review-comments.json必须是标准对象结构(根对象含comments数组,元素含comment_id/path/line/side/body);若不满足,视为上游脚本错误,必须先修正上游脚本。- 评论项必须包含
thread_resolved布尔字段;流程状态仅由该字段决定(true -> resolved,false -> open)。 snapshot/必须保留源码相对路径层级,禁止平铺文件名。示例:snapshot/round-2/files/core/ebpf/protocol/redis/RedisParser.cpp。若出现平铺结果,视为快照脚本错误或中途中断,必须重跑修正。
4. 生成/更新评论状态文件:
- PR:
python3 .cursor/skills/code-review/scripts/update_comment_status.py --repo-root <repo> --target-type pr --target-id <pr> - 分支:
python3 .cursor/skills/code-review/scripts/update_comment_status.py --repo-root <repo> --target-type branch --target-id <branchName> - 说明:这一步只同步结构与流程状态(
status_flow)并保留历史status_tech,不会自动做代码复核判定。
5. 生成双维状态 Markdown 报告(表格):
- PR:
python3 .cursor/skills/code-review/scripts/generate_comment_status_report.py --repo-root <repo> --target-type pr --target-id <pr> - 分支:
python3 .cursor/skills/code-review/scripts/generate_comment_status_report.py --repo-root <repo> --target-type branch --target-id <branchName> - 输出文件固定为:
comments/comment-status.md(列:评论时间、文件、行号、作者、评论、流程状态、技术状态)
6. 计算增量映射与回退建议(--base 与 --head 必须传 commit SHA):
- PR:
python3 .cursor/skills/code-review/scripts/incremental_review_mapper.py --repo-root <repo> --target-type pr --target-id <pr> --base <baseSha> --head <headSha> --review-round <n> - 分支:
python3 .cursor/skills/code-review/scripts/incremental_review_mapper.py --repo-root <repo> --target-type branch --target-id <branchName> --base <baseSha> --head <headSha> --review-round <n> - 当
snapshot/latest.json存在时,映射脚本会计算snapshot_match_rate,用于 rebase 冲突调整或 squash 合并后的增量决策辅助。
7. 根据脚本输出中的 recommendation 执行:
incremental:只评审need_review_commitspartial:优先评审need_review_commits,并补审低置信 hunkfull:执行全量评审,但必须做历史意见去重
8. 技术状态(status_tech)必须逐条复核,不允许猜测:
- 必读输入(按顺序):
1) comments/review-comments.json 2) comments/comment-status.json 3) reviewed_commits.json 4) 当前代码中与 comment path 对应文件 5) snapshot/ 中同路径历史快照文件(若存在)
- 逐条处理规则(按
comment_id): - 仅允许更新:
status_tech、mapped_finding_id、notes status_tech仅可取:fixed|not-fixed|false-positive|partially-fixednotes必须写明“判定证据”,至少包含:对比文件、关键代码变化、结论原因- 每轮必须优先复核上一轮未终态条目(
not-fixed、partially-fixed)。 - 人工手动订正(支持):
- 若评论作者本人(当前
gh登录账号)在该评论线程回复文本包含fixed,状态同步为fixed。 - 若回复文本包含
false-positive(或false positive),状态同步为false-positive。 - 手动订正由脚本在更新
comment-status.json时自动吸收,并写入notes。 - 终态跳过规则(默认开启):
- 当前
status_tech为fixed或false-positive的条目,本轮默认跳过技术复核。 - 仅在以下条件触发时重开复核:
1) 条目 path 在本轮 commit 范围内再次发生修改; 2) 条目 status_flow 从 resolved 变为非 resolved; 3) 人工显式指定强制复核(按 comment_id 列表)。
- 输出要求:
- 更新后的
comments/comment-status.json - 重新生成
comments/comment-status.md
9. 本轮评审收尾后,必须生成 snapshot 供下一轮增量决策使用:
- PR:
python3 .cursor/skills/code-review/scripts/build_snapshot.py --repo-root <repo> --target-type pr --target-id <pr> --base <baseSha> --head <headSha> --review-round <n> - 分支:
python3 .cursor/skills/code-review/scripts/build_snapshot.py --repo-root <repo> --target-type branch --target-id <branchName> --base <baseSha> --head <headSha> --review-round <n> - 产物:
snapshot/round-<n>/files/*、snapshot/round-<n>/manifest.json、snapshot/latest.json
状态文件字段约束(必须遵守):
reviewed_commits.json记录:commit_shapatch_id(用于 rebase 后精确映射)review_roundreviewed_athunk_fingerprints(数组)comments/comment-status.json记录:comment_idpath/line/sidebodysnippet(可读代码片段)snippet_fingerprint(规范化片段 hash)status_flow(open|resolved|wont-fix|deferred)status_tech(fixed|not-fixed|false-positive|partially-fixed)mapped_finding_id
说明:
snippet_fingerprint定义为“规范化代码片段 + 文件路径 + 评论定位三元组(line/side/comment_id)”的稳定 hash,不能只用行号。- 允许人工修正
status_flow与status_tech,但不得删除历史记录。
增量评审策略(必须执行):
1. 优先读取 reviewed_commits.json,只评审未覆盖的新变更。 2. 若检测到 rebase/force-push,不可直接判定全量重审,先做映射再决策:
- L1(高置信):按
patch-id映射旧 commit -> 新 commit,命中后继承“已评审”状态。 - L2(中置信):按
path + 规范化 hunk 片段 + hunk 上下文做指纹匹配,仅补审未命中 hunk。 - L3(低置信):命中率低或冲突改写明显时,回退全量评审。
3. 置信度门槛默认:
commit_map_rate >= 90%:增量通过hunk_match_rate >= 80%:局部补审- 否则全量回退
4. 即使全量回退,也必须复用历史评论与 finding 去重,避免重复意见。
snapshot 在增量决策中的职责(必须遵守):
1. snapshot 是增量决策辅助依据,不替代 git 主链路(patch-id/hunk)。 2. rebase 且发生冲突改写时,若 commit/hunk 映射不足,可使用 snapshot_match_rate 辅助从 full 降到 partial。 3. squash 合并导致 commit 边界丢失时,snapshot_match_rate 用于判断是否可继续增量评审。 4. 若 snapshot_match_rate 不足阈值,仍必须 full 全量评审。
Phase 2: Context Building(全局认知)
开始评审前,必须先完成以下步骤:
1. 读取 /.cursor/rules/project-knowledge/architecture.md,建立系统架构和模块职责认知。 2. 读取 /.cursor/rules/project-knowledge/codebase-map.md,优先吸收:
- 公共能力入口(必须复用的 common/helper)
- 生命周期与资源释放不变量
- 配置/环境变量约定(兼容大小写、默认值、废弃参数映射)
- 历史 review 高频问题(作为优先检查清单)
3. 读取并参考以下规范(按变更涉及范围选择):
/.cursor/skills/selfmonitor/SKILL.md(自监控与告警相关改动必读)/.cursor/skills/security-check/SKILL.md(安全与合规相关改动必读)/.cursor/skills/compile/SKILL.md(涉及构建/编译链路时必读)
4. 基于 PR/分支变更列表,读取受影响文件的完整上下文(至少覆盖变更函数、调用方、定义处)。 5. 若改动涉及 pipeline/runner/配置系统,必须先阅读以下代码再下结论:
core/application/Application.cpp(主循环、配置扫描、退出顺序)core/collection_pipeline/CollectionPipelineManager.cppcore/collection_pipeline/CollectionPipeline.cppcore/runner/ProcessorRunner.cppcore/runner/FlusherRunner.cppcore/config/watcher/PipelineConfigWatcher.cppcore/config/OnetimeConfigInfoManager.cppcore/file_server/FileServer.cppcore/file_server/checkpoint/CheckPointManager.cppcore/file_server/checkpoint/CheckpointManagerV2.cpp(改动涉及 exactly-once 时)
6. 通过 MCP/gh 工具拉取评审上下文:
- PR 描述、提交历史、PR review comments、CI 状态
- 最近约 10 个相关 PR 的 review 评论(提炼团队偏好)
7. 若可访问 Code 平台历史评论,优先抽样最近已合入 PR 的 review comments(建议>=30条)并做“模式交叉”:
- 把历史高频问题映射到本次变更文件,标记为“高风险检查项”
- 若与
codebase-map冲突,以“最新代码事实 + 评论证据”更新结论
8. 若发现历史约束或设计决策冲突,先记录“假设与证据”,后续在报告中显式说明。
Phase 3: Intent Analysis(意图理解)
完成上下文分析后,必须先产出“理解文档”,再进入问题列表。该文档是给开发者学习和理解代码用的,不能省略。
Phase 3 输出要求(必须输出文档)
必须输出一个独立文档(建议标题:Code Review - Intent & Architecture Notes),至少包含:
- 作者意图:这个 PR/分支要解决什么问题,为什么现在做。
- 端到端流程:从入口到出口,这次变更实际改变了哪些关键路径。
- 影响范围:涉及哪些模块、接口、配置、状态文件、监控指标、告警链路。
- 预期结果验证:改动是否达到目标,并给出证据与推理过程。
Phase 3 落盘要求(必须写入 code-review 目录)
必须将 Phase 3 文档写入仓库 code-review/ 目录,禁止只在聊天中输出。
建议路径:
- PR 评审:
code-review/pr-<number>/intent-architecture-notes.md - 分支评审:
code-review/branch-<branchName>/intent-architecture-notes.md(/替换为-)
要求:
- 若目录不存在必须先创建。
- 文档顶部必须包含评审对象元信息(PR号/分支名、commit范围、生成时间)。
Mermaid 可视化要求(必须至少 2 张图)
该理解文档必须包含 Mermaid 图,用于帮助学习与沟通。按改动内容选择,至少输出以下 2 类中的 2 张:
- 架构图(模块关系 / 依赖边界)
- 流程图(关键执行路径)
- 时序图(组件交互、调用顺序、异步/重试行为)
- 数据结构图(关键状态对象、队列、checkpoint 主从关系)
建议:
- 小改动:至少 2 张图(流程 + 时序)
- 中大型改动:3-4 张图(架构 + 流程 + 时序 + 数据结构)
注意:
- 图必须与当前变更强相关,禁止画与本次 PR 无关的“百科全图”。
- 图中节点命名使用代码中的真实组件/类型名称,避免抽象空词。
- Mermaid 语法请遵循
/.cursor/skills/mermaid/SKILL.md。
牢记评估标准(无需输出)
对每个变更文件和差异块,按以下 6 组标准检查:
1. 业务与架构:目标达成、职责边界、拓扑与依赖、故障传播。 2. 正确性与安全:边界检查、类型/异常处理、外部输入防御、安全合规。 3. 并发与生命周期:线程/锁/队列正确退出、资源释放、状态恢复。 4. 性能与资源:热路径复杂度、拷贝与分配、容量上限、日志开销。 5. 稳定性与可观测:指标/日志/告警完整性与可定位性。 6. 可维护性、兼容性与文档测试:可读性、向后兼容、文档与测试覆盖。
注意:以上不是“通用建议列表”,而是必须落到每个 sub-agent 的责任范围中执行(见下一节责任矩阵)。
Phase 4: Sub-agent Review(专项检查)
并行启动专项 sub-agent(建议 3-4 个并行,避免过度拆分)。每个 sub-agent 独立输出“发现的问题 + 证据”。 每个 sub-agent 必须引用“牢记评估标准”中对应条目,不得只做口头判断。 每个问题必须标注来源标准编号(例如:[S3] 表示“并发与生命周期”)。
责任矩阵(主责/次责)
- Sub-agent A(逻辑与架构):主责
S1,次责S6 - Sub-agent B(并发与生命周期):主责
S3,次责S5 - Sub-agent C(安全稳定与性能):主责
S2+S4,次责S5 - Sub-agent D(复用、兼容、文档测试):主责
S6,次责S1+S5
规则:
- 主责标准必须全量覆盖;次责标准只需覆盖与本次改动直接相关的部分。
- 若某问题跨多个标准,允许多标记(如
[S2][S4])。 - 不允许多个 agent 报告同一问题的重复结论;若重复,保留证据更完整的一条。
Sub-agent A: 逻辑正确性与架构一致性
- 业务逻辑是否完整,是否存在边界漏处理、状态不一致、错误传播断裂。
- 与 LoongCollector 架构约束是否一致(输入/处理/输出职责、Runner 模式、配置注册模式)。
- 是否引入隐式依赖、循环依赖或故障传播不可观测的问题。
- 重点覆盖评估标准:业务与架构、可维护性与兼容性。
Sub-agent B: 并发、异步与生命周期
- 锁粒度、锁顺序、数据竞争、线程退出路径是否安全。
- 回调/异步流程是否存在竞态、悬空引用、未处理失败路径。
- 新增线程/定时任务是否可控停止,是否符合项目既有模式。
- 重点覆盖评估标准:并发与生命周期、稳定性与可观测。
- 生命周期/资源管理必查细则(必须逐项核对,重点是“正确释放与状态恢复”):
- 资源释放闭环:
- 每条路径(启动失败、热更新替换、删除配置、进程退出)都要核对资源闭环:
- 线程/future 可退出并被回收
- queue pop 被 disable 后不再悬挂
- 插件/Go pipeline 可停止且不残留引用
- flush/batch/checkpoint 落盘语义与路径一致
- 死锁与卡死风险:
- 锁顺序是否跨模块一致(pipeline manager / queue manager / file server)。
WaitAllItemsInProcessFinished、队列Wait/Trigger、HoldOn/Resume是否可能形成循环等待。- 长等待仅告警不终止的路径,是否可能导致永久卡住或退出超时。
- 状态恢复正确性(核心):
- 热加载后是否恢复到“可继续采集+处理+发送”的一致状态,而非部分组件已恢复。
- 文件采集
Pause -> Dump -> ClearCache -> Resume后,handler/checkpoint/缓存三者是否一致。 - 配置失败回滚时,旧 pipeline/task 是否保持可用,不出现半更新状态。
- 顺序检查作为辅证(不是唯一判据):
- 仍需核对关键顺序(runner init 顺序、pipeline start/stop 顺序),但结论必须落到资源与状态结果。
Sub-agent C: 安全、稳定性与性能
- 输入校验、异常处理、重试退避、资源释放(RAII)是否完备。
- 右值/所有权:核验【调用点-传参-消费点】全链路,防止异常转移或冗余拷贝。
- 是否存在热路径性能回退(重复计算、拷贝、容器增长失控、高频日志刷屏)。
- 监控指标/告警是否完整,是否满足自监控规范。
- 重点覆盖评估标准:正确性与安全、性能与资源、稳定性与可观测。
- Checkpoint 必查细则(按改动范围选择):
- onetime checkpoint:
- 启动时
LoadCheckpointFile(),配置变化后DumpCheckpointFile()。 - 超时删除、
RemoveConfig()与 checkpoint 文件是否保持一致,避免残留条目导致错误恢复。 - file checkpoint(v1):
FileServer::Start()是否仍保持LoadCheckPoint()在前、注册 handler 在后。Pause/Stop是否保证DumpCheckPointToLocal(),以及失败场景是否有可定位日志/告警。- exactly-once checkpoint(v2):
- 主 checkpoint 与 range checkpoint 是否成对维护,避免孤儿 key。
- 扫描与 GC 逻辑是否可能误删活跃 checkpoint,或导致恢复时状态不连续。
Sub-agent D: 复用合规与文档一致性
- 是否重复实现了已有公共能力(优先复用
core/common与现有工具函数)。 - 注释与代码行为是否一致,TODO/FIXME 是否引入新技术债。
- 插件配置或
GetXxxParam改动是否同步更新docs/对应文档。 - 重点覆盖评估标准:可维护性、兼容性与文档测试。
Phase 5: Final Report(最终输出)
Final Report 偏实用交付,可直接用于落地修复和平台流转。它与 Phase 2 的“理解文档”并行存在、互不替代。
Phase 5 输出要求(实用导向)
1. 先给 Findings,按严重度排序:Critical > High > Medium > Low。 2. 每个问题必须包含可定位证据与可执行建议。 3. 若未发现问题,明确写出“未发现阻断问题”,并列出残余风险与测试缺口。 4. 最后补充 Highlights(正向实践),简洁即可。 5. 必须包含 Lifecycle Verdict:
- 资源释放:
PASS/FAIL - 死锁/卡死风险:
PASS/FAIL - 状态恢复正确性:
PASS/FAIL - 每项附 1-3 条证据。
6. 必须包含 Fix Plan(按优先级分组):
- 立即修复(阻断合入)
- 合入前修复
- 可后续改进
7. 必须包含 Validation Plan(修复后怎么验证):
- 需要跑哪些测试、观察哪些指标、验证哪些告警与恢复路径。
Final Report 落盘要求(必须写入 code-review 目录)
必须将 Final Report 写入仓库 code-review/ 目录,禁止只在聊天中输出。
建议路径(与 Phase 2 同目录):
- PR 评审:
code-review/pr-<number>/final-report.md - 分支评审:
code-review/branch-<branchName>/final-report.md(/替换为-)
要求:
final-report.md必须引用对应的intent-architecture-notes.md(相对路径链接)。- 若执行了平台发布(PR评论/Review),在文档末尾记录发布链接;若失败,记录失败原因与重试命令。
问题输出格式:
- Severity: <Critical|High|Medium|Low>
- File: [<路径>:<起始行号>](file://./<路径>#L<起始行号>)
- 问题: <一句话说明问题本质>
- 影响: <可能导致的错误行为/风险>
- 建议: <可直接执行的修复建议,必要时给最小代码片段>额外要求:
- 行号必须在最终输出前重新核对,确保可点击跳转。
- 仅评论真实变更范围内的问题,避免“顺手重构建议”淹没核心缺陷。
- 语气专业、直接、简洁,优先给出可验证结论。
平台发布(可选但推荐)
若当前评审场景是 PR/分支评审,且工具可用,请在用户要求发布后自动化发布 Final Report:
- 必须等待用户显式确认后才能执行发布。
- 发布结构:
1) Inline Findings:将可定位的问题逐条作为代码行内评论发布(不是回复到 PR 主评论)。 2) PR 摘要评论:将Final Report 摘要回复到 PR 主评论。
- 必含:Critical/High/Medium/Low 数量统计表、Lifecycle PASS/FAIL 表格、Lifecycle FAIL 证据、总体结论、Highlights。
- 不含:不重复贴全部 findings。
- 发布工具:
- 优先使用
gh工具提交结构化评审结果;若环境存在 GitHub MCP,可等价使用 MCP。 - Inline 评论建议使用
gh api repos/<owner>/<repo>/pulls/<pr>/comments(需包含commit_id/path/line/side/body)。 - 摘要评论建议使用
gh pr comment <pr> --body-file <summary.md>。 - 若发布失败,必须在输出中说明失败原因并给出可复制的发布内容。
{
"version": "1.0",
"generated_at": "",
"review_target": {
"type": "pr",
"id": ""
},
"status": [
{
"comment_id": 0,
"path": "",
"line": 0,
"side": "RIGHT",
"body": "",
"snippet": "",
"snippet_fingerprint": "",
"status_flow": "open",
"status_tech": "not-fixed",
"mapped_finding_id": "",
"notes": ""
}
]
}
Code-Review Failure Playbook
本文件是故障恢复决策表,目标是让 agent 在异常时做正确分流:自动恢复、回退流程、或请求人工介入。
总原则
- 优先判断当前是首次还是非首次。
- 不手工拼接 JSON;恢复后必须回到标准流程节点继续执行。
- Preflight 相关异常默认人工介入,其余优先自动回退到可重建节点。
- 若脚本失败但不影响代码读取,允许降级继续评审,同时必须输出失败反馈记录。
场景 1:Preflight 失败(人工介入)
- 触发信号:
python3 --version/git rev-parse --is-inside-work-tree/gh auth status任一失败 - 决策:停止自动执行,提示用户介入检查环境与认证
- 动作级别:
manual_required - 返回节点:Preflight(三条检查全部通过后再进入 Phase 1)
场景 2:首次运行缺文件(正常入口,不是失败)
- 触发信号:
code-review/<target>/不存在,或缺少meta.json/reviewed_commits.json/comments/* - 决策:判定为 Bootstrap,走初始化流程
- 动作级别:
auto_recover - 返回节点:Phase 1-步骤 1(初始化)并顺序继续
场景 3:非首次运行时输入 schema 非法
- 触发信号:
invalid review-comments.json/invalid comment-status.json - 决策:放弃损坏中间态,回退到 Bootstrap 重建关键输入
- 动作级别:
auto_recover - 返回节点:Phase 1-步骤 1(初始化)-> 步骤 2(拉取 comments)-> 步骤 3(重建状态)
场景 4:commit 对象缺失 / commit 范围构建失败
- 触发信号:
missing base/head commit object或failed to build commit range - 决策:先自动同步 git 对象;若仍失败,转人工确认 base/head 选择
- 动作级别:
auto_then_manual - 返回节点:
- 自动恢复成功:Phase 1-步骤 5(增量映射)
- 自动恢复失败:人工确认后重跑步骤 5
场景 5:snapshot 目录平铺
- 触发信号:
snapshot/下没有源码相对路径层级(仅平铺文件) - 决策:视为快照过程异常,清空并重建快照
- 动作级别:
auto_recover - 返回节点:快照生成步骤(完成后继续技术状态复核)
场景 6:脚本运行异常但可继续评审
- 触发信号:任意脚本报错,但仓库代码与基础 git/gh 能力仍可读取
- 决策:允许降级继续评审,避免流程阻塞;并强制记录失败反馈用于迭代 skill
- 动作级别:
degrade_continue - 必做动作:
- 写入
code-review/<target>/script-failures.md(脚本名、命令、错误、时间、补偿动作) - 评审策略一律切换到
full全量评审 - 在
final-report.md增加 “Script Failure Feedback” 小节 - 返回节点:当前评审阶段(按降级策略继续)
动作级别定义
manual_required:必须人工介入后才能继续auto_recover:agent 可自动恢复并继续流程auto_then_manual:先自动尝试,失败后升级人工degrade_continue:允许继续评审,但必须记录失败并反馈
{
"version": "1.0",
"repo": "",
"review_target": {
"type": "pr",
"id": "",
"base_ref": "",
"head_ref": "",
"base_sha": "",
"head_sha": ""
},
"strategy": {
"commit_map_threshold": 0.9,
"hunk_match_threshold": 0.8,
"fallback_on_low_confidence": true
},
"review_round": 1,
"generated_at": ""
}
Review Plan
- Review Target:
<pr-xxxx | branch-name> - Base SHA:
<base_sha> - Head SHA:
<head_sha> - Strategy:
<incremental | partial | full> - Current Phase:
<Phase x in_progress>
Work Items
请按本轮实际任务填写,不要只写 Phase 名称。
建议格式:每个大项下拆 2~5 个子项,并用 checkbox 跟踪进度。
<Major Item 1>
- [ ] <Sub-item 1>
- [ ] <Sub-item 2>
<Major Item 2>
- [ ] <Sub-item 1>
- [ ] <Sub-item 2>
Risks / Blockers
- <None / details>
Notes
- 若策略切换(例如
incremental -> full),先更新本文件再继续执行。
{
"version": "1.0",
"review_rounds": [],
"commits": [
{
"commit_sha": "",
"patch_id": "",
"review_round": 1,
"reviewed_at": "",
"hunk_fingerprints": [],
"files": [],
"mapping": {
"method": "direct|patch-id|hunk-similarity|none",
"mapped_from_commit": "",
"confidence": 0.0
}
}
]
}
#!/usr/bin/env python3
import argparse
import hashlib
import json
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Set, Tuple
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def run_git(repo_root: Path, args: List[str]) -> str:
proc = subprocess.run(["git", *args], cwd=repo_root, text=True, capture_output=True, check=True)
return proc.stdout
def run_git_no_check(repo_root: Path, args: List[str]) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], cwd=repo_root, text=True, capture_output=True, check=False)
def normalize_file_content(text: str) -> str:
lines = [re.sub(r"\s+", " ", line.strip()) for line in text.splitlines()]
return "\n".join(lines)
def stable_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def get_changed_files(repo_root: Path, base_sha: str, head_sha: str) -> List[str]:
out = run_git(repo_root, ["diff", "--name-only", f"{base_sha}..{head_sha}"])
return sorted({line.strip() for line in out.splitlines() if line.strip()})
def get_file_content_at_commit(repo_root: Path, commit_sha: str, path: str) -> str:
proc = run_git_no_check(repo_root, ["show", f"{commit_sha}:{path}"])
if proc.returncode != 0:
return ""
return proc.stdout
def main() -> None:
parser = argparse.ArgumentParser(description="Build snapshot baseline for incremental review.")
parser.add_argument("--repo-root", required=True)
parser.add_argument("--target-type", choices=["pr", "branch"])
parser.add_argument("--target-id")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
parser.add_argument("--base", required=True, help="Base commit SHA")
parser.add_argument("--head", required=True, help="Head commit SHA")
parser.add_argument("--review-round", required=True, type=int)
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
snapshot_root = review_dir / "snapshot" / f"round-{args.review_round}"
files_root = snapshot_root / "files"
files_root.mkdir(parents=True, exist_ok=True)
changed_files = get_changed_files(repo_root, args.base, args.head)
manifest_files: List[Dict[str, object]] = []
for rel_path in changed_files:
content = get_file_content_at_commit(repo_root, args.head, rel_path)
if content == "":
# Deleted file at head; keep entry for audit but no content snapshot.
manifest_files.append(
{"path": rel_path, "exists_in_head": False, "raw_hash": "", "normalized_hash": "", "size": 0}
)
continue
out_path = files_root / rel_path
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(content, encoding="utf-8")
manifest_files.append(
{
"path": rel_path,
"exists_in_head": True,
"raw_hash": stable_hash(content),
"normalized_hash": stable_hash(normalize_file_content(content)),
"size": len(content.encode("utf-8")),
}
)
manifest = {
"version": "1.0",
"review_target": {"type": target_type, "id": target_id_raw},
"review_round": args.review_round,
"base_sha": args.base,
"head_sha": args.head,
"generated_at": utc_now(),
"files": manifest_files,
}
manifest_path = snapshot_root / "manifest.json"
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
latest = {
"latest_round": args.review_round,
"manifest": str(manifest_path.relative_to(review_dir)),
"updated_at": utc_now(),
}
latest_path = review_dir / "snapshot" / "latest.json"
latest_path.parent.mkdir(parents=True, exist_ok=True)
latest_path.write_text(json.dumps(latest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(
json.dumps(
{
"review_target": {"type": target_type, "id": target_id_raw},
"review_round": args.review_round,
"files": len(manifest_files),
"manifest": str(manifest_path),
},
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Tuple
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def run_cmd(args: List[str], cwd: Path) -> str:
proc = subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=False)
if proc.returncode != 0:
raise SystemExit(f"command failed: {' '.join(args)}\n{proc.stderr.strip()}")
return proc.stdout
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def parse_name_with_owner(repo_root: Path) -> Tuple[str, str]:
out = run_cmd(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], repo_root).strip()
if "/" not in out:
raise SystemExit(f"invalid repository nameWithOwner: {out}")
owner, name = out.split("/", 1)
return owner, name
def get_viewer_login(repo_root: Path) -> str:
out = run_cmd(["gh", "api", "user", "--jq", ".login"], repo_root).strip()
return out
def run_graphql(repo_root: Path, owner: str, name: str, pr_number: int, cursor: str) -> Dict[str, Any]:
# Query review threads instead of plain review comments so we can
# persist thread-level resolution state deterministically.
query = """
query($owner:String!, $name:String!, $number:Int!, $endCursor:String) {
repository(owner:$owner, name:$name) {
pullRequest(number:$number) {
reviewThreads(first:100, after:$endCursor) {
pageInfo { hasNextPage endCursor }
nodes {
isResolved
comments(first:100) {
nodes {
databaseId
body
path
line
originalLine
createdAt
updatedAt
author { login }
originalCommit { oid }
replyTo { databaseId }
}
}
}
}
}
}
}
"""
cmd = [
"gh",
"api",
"graphql",
"-f",
f"query={query}",
"-F",
f"owner={owner}",
"-F",
f"name={name}",
"-F",
f"number={pr_number}",
]
if cursor:
cmd.extend(["-F", f"endCursor={cursor}"])
out = run_cmd(cmd, repo_root)
return json.loads(out)
def main() -> None:
parser = argparse.ArgumentParser(description="Fetch PR review comments to stable schema file.")
parser.add_argument("--repo-root", required=True)
parser.add_argument("--target-type", choices=["pr", "branch"])
parser.add_argument("--target-id")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
comments_path = review_dir / "comments" / "review-comments.json"
comments_path.parent.mkdir(parents=True, exist_ok=True)
if target_type != "pr":
payload = {
"version": "1.0",
"source": "branch_review_comments",
"fetched_at": utc_now(),
"review_target": {"type": target_type, "id": target_id_raw},
"comments": [],
}
comments_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"target": f"{target_type}-{target_id_raw}", "threads": 0, "comments": 0, "resolved_threads": 0}))
return
owner, name = parse_name_with_owner(repo_root)
viewer_login = get_viewer_login(repo_root)
pr_number = int(target_id_raw)
cursor = ""
has_next = True
comments: List[Dict[str, Any]] = []
total_threads = 0
resolved_threads = 0
while has_next:
# Paginate until all review threads are collected.
data = run_graphql(repo_root, owner, name, pr_number, cursor)
threads_obj = data["data"]["repository"]["pullRequest"]["reviewThreads"]
page_info = threads_obj["pageInfo"]
threads = threads_obj["nodes"] or []
total_threads += len(threads)
for thread in threads:
is_resolved = bool(thread.get("isResolved", False))
if is_resolved:
resolved_threads += 1
thread_comments = thread.get("comments", {}).get("nodes", []) or []
for c in thread_comments:
author = (c.get("author") or {}).get("login", "")
original_commit = (c.get("originalCommit") or {}).get("oid", "")
reply_to = (c.get("replyTo") or {}).get("databaseId")
# Use originalLine as a stable anchor because line can be null
# after code evolves on newer commits.
original_line = c.get("originalLine")
line = original_line if isinstance(original_line, int) else 0
comments.append(
{
"comment_id": c.get("databaseId"),
"author": author,
"created_at": c.get("createdAt", ""),
"updated_at": c.get("updatedAt", ""),
"path": c.get("path", ""),
"line": line,
"side": "RIGHT",
"commit_id": original_commit,
"in_reply_to_id": reply_to,
"body": c.get("body", ""),
"thread_resolved": is_resolved,
}
)
has_next = bool(page_info.get("hasNextPage"))
cursor = page_info.get("endCursor") if has_next else ""
payload = {
"version": "1.0",
"source": "github_pr_review_comments",
"fetched_at": utc_now(),
"review_target": {"type": "pr", "id": target_id_raw},
"viewer_login": viewer_login,
"comments": comments,
}
comments_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(
json.dumps(
{
"target": f"pr-{target_id_raw}",
"threads": total_threads,
"comments": len(comments),
"resolved_threads": resolved_threads,
},
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
from typing import Dict, List, Tuple
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def read_json(path: Path) -> Dict:
if not path.exists():
raise SystemExit(f"missing file: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def esc_cell(text: str) -> str:
return (text or "").replace("\n", " ").replace("|", "\\|").replace("`", "").strip()
def build_comment_meta_map(review_comments_payload: Dict) -> Dict[int, Dict]:
comments = review_comments_payload.get("comments", [])
meta_map: Dict[int, Dict] = {}
if not isinstance(comments, list):
return meta_map
for c in comments:
cid = c.get("comment_id")
if isinstance(cid, int):
meta_map[cid] = c
return meta_map
def build_markdown(target_type: str, target_id: str, items: List[Dict], comment_meta: Dict[int, Dict]) -> str:
lines = []
lines.append(f"# Comment Status Report ({target_type}-{target_id})")
lines.append("")
lines.append(f"- Total: {len(items)}")
lines.append("")
lines.append("| 评论时间 | File | Line | 作者 | Comment | Flow | Tech |")
lines.append("|---|---|---:|---|---|---|---|")
for item in items:
cid = item.get("comment_id", "")
meta = comment_meta.get(cid, {})
created_at = esc_cell(str(meta.get("created_at", "")))
author = esc_cell(str(meta.get("author", "")))
path = esc_cell(str(item.get("path", "")))
line = item.get("line", 0)
body = esc_cell(str(item.get("body", "")))
if len(body) > 160:
body = body[:157] + "..."
status_flow = esc_cell(str(item.get("status_flow", "")))
status_tech = esc_cell(str(item.get("status_tech", "")))
lines.append(
f"| {created_at} | `{path}` | {line} | {author} | {body} | {status_flow} | {status_tech} |"
)
lines.append("")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description="Generate markdown report from comment-status.json.")
parser.add_argument("--repo-root", required=True)
parser.add_argument("--target-type", choices=["pr", "branch"])
parser.add_argument("--target-id")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
status_path = review_dir / "comments" / "comment-status.json"
review_comments_path = review_dir / "comments" / "review-comments.json"
report_path = review_dir / "comments" / "comment-status.md"
payload = read_json(status_path)
if not isinstance(payload, dict) or not isinstance(payload.get("status"), list):
raise SystemExit("invalid comment-status.json: root must be object and `status` must be list")
review_comments_payload = read_json(review_comments_path)
if not isinstance(review_comments_payload, dict):
raise SystemExit("invalid review-comments.json: root must be object")
comment_meta = build_comment_meta_map(review_comments_payload)
markdown = build_markdown(target_type, target_id_raw, payload["status"], comment_meta)
report_path.write_text(markdown + "\n", encoding="utf-8")
print(str(report_path))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import hashlib
import json
import re
import subprocess
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, List, Optional, Set, Tuple
def run_git(repo_root: Path, args: List[str]) -> str:
result = subprocess.run(
["git", *args],
cwd=repo_root,
text=True,
capture_output=True,
check=True,
)
return result.stdout
def run_git_no_check(repo_root: Path, args: List[str]) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args],
cwd=repo_root,
text=True,
capture_output=True,
check=False,
)
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def normalize_code_line(line: str) -> str:
return re.sub(r"\s+", " ", line.strip())
def stable_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def normalize_file_content(text: str) -> str:
# Keep line boundaries but normalize whitespace noise for robust matching.
lines = [re.sub(r"\s+", " ", line.strip()) for line in text.splitlines()]
return "\n".join(lines)
def compute_patch_id(repo_root: Path, commit_sha: str) -> str:
patch_text = run_git(repo_root, ["show", "--pretty=format:", "--no-color", commit_sha])
proc = subprocess.run(
["git", "patch-id", "--stable"],
cwd=repo_root,
text=True,
input=patch_text,
capture_output=True,
check=True,
)
output = proc.stdout.strip()
return output.split()[0] if output else ""
def get_commit_files(repo_root: Path, commit_sha: str) -> List[str]:
out = run_git(repo_root, ["show", "--pretty=format:", "--name-only", "--no-color", commit_sha])
return sorted({line.strip() for line in out.splitlines() if line.strip()})
def get_file_content_at_commit(repo_root: Path, commit_sha: str, path: str) -> Optional[str]:
proc = run_git_no_check(repo_root, ["show", f"{commit_sha}:{path}"])
if proc.returncode != 0:
return None
return proc.stdout
def load_latest_snapshot_map(review_dir: Path) -> Dict[str, str]:
latest_path = review_dir / "snapshot" / "latest.json"
if not latest_path.exists():
return {}
try:
latest = json.loads(latest_path.read_text(encoding="utf-8"))
except Exception:
return {}
manifest_rel = latest.get("manifest")
if not isinstance(manifest_rel, str) or not manifest_rel:
return {}
manifest_path = review_dir / manifest_rel
if not manifest_path.exists():
return {}
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
return {}
mapping: Dict[str, str] = {}
for item in manifest.get("files", []):
path = item.get("path")
n_hash = item.get("normalized_hash")
if isinstance(path, str) and isinstance(n_hash, str):
mapping[path] = n_hash
return mapping
def compute_snapshot_match_rate(
repo_root: Path, head_sha: str, changed_files: Set[str], snapshot_map: Dict[str, str]
) -> Optional[float]:
if not snapshot_map or not changed_files:
return None
overlap = [p for p in changed_files if p in snapshot_map]
if not overlap:
return None
matched = 0
for path in overlap:
content = get_file_content_at_commit(repo_root, head_sha, path)
if content is None:
continue
current_hash = stable_hash(normalize_file_content(content))
if current_hash == snapshot_map[path]:
matched += 1
return matched / len(overlap)
def parse_hunk_fingerprints(repo_root: Path, commit_sha: str) -> List[str]:
patch = run_git(repo_root, ["show", "--pretty=format:", "--no-color", "-U3", commit_sha])
lines = patch.splitlines()
file_path = ""
hunk_header = ""
hunk_lines: List[str] = []
fps: List[str] = []
def flush() -> None:
nonlocal hunk_lines, hunk_header
if not hunk_lines:
return
key = file_path + "\n" + hunk_header + "\n" + "\n".join(hunk_lines)
fps.append(stable_hash(key))
hunk_lines = []
hunk_header = ""
for line in lines:
if line.startswith("diff --git "):
flush()
m = re.search(r" b/(.+)$", line)
file_path = m.group(1) if m else ""
continue
if line.startswith("@@"):
flush()
hunk_header = line
continue
if line.startswith("+") or line.startswith("-"):
if line.startswith("+++") or line.startswith("---"):
continue
hunk_lines.append(normalize_code_line(line[1:]))
flush()
return sorted(set(fps))
def jaccard(a: Set[str], b: Set[str]) -> float:
if not a and not b:
return 1.0
if not a or not b:
return 0.0
return len(a & b) / len(a | b)
@dataclass
class CommitRecord:
commit_sha: str
patch_id: str
hunk_fingerprints: List[str]
review_round: int
reviewed_at: str
mapping: Dict[str, object]
def load_json(path: Path) -> Dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def save_json(path: Path, payload: Dict) -> None:
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def ensure_commit_exists(repo_root: Path, sha: str, target_type: str, target_id: str) -> bool:
exists = run_git_no_check(repo_root, ["cat-file", "-e", f"{sha}^{{commit}}"])
if exists.returncode == 0:
return True
# First generic fetch to cover normal branch updates.
run_git_no_check(repo_root, ["fetch", "--all", "--prune", "--tags"])
exists = run_git_no_check(repo_root, ["cat-file", "-e", f"{sha}^{{commit}}"])
if exists.returncode == 0:
return True
# Then PR-specific fetch for detached PR heads.
if target_type == "pr":
run_git_no_check(repo_root, ["fetch", "origin", f"pull/{target_id}/head"])
exists = run_git_no_check(repo_root, ["cat-file", "-e", f"{sha}^{{commit}}"])
if exists.returncode == 0:
return True
return False
def main() -> None:
parser = argparse.ArgumentParser(description="Map reviewed commits for incremental PR/branch review.")
parser.add_argument("--repo-root", required=True)
parser.add_argument("--target-type", choices=["pr", "branch"])
parser.add_argument("--target-id")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
parser.add_argument("--base", required=True, help="Base commit SHA for comparison")
parser.add_argument("--head", required=True, help="Head commit SHA for comparison")
parser.add_argument("--review-round", required=True, type=int)
parser.add_argument("--commit-map-threshold", type=float, default=0.9)
parser.add_argument("--hunk-match-threshold", type=float, default=0.8)
parser.add_argument("--snapshot-match-threshold", type=float, default=0.9)
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
reviewed_path = review_dir / "reviewed_commits.json"
reviewed = load_json(reviewed_path) or {"version": "1.0", "review_rounds": [], "commits": []}
old_commits = reviewed.get("commits", [])
snapshot_map = load_latest_snapshot_map(review_dir)
if not ensure_commit_exists(repo_root, args.base, target_type, target_id_raw):
raise SystemExit(
f"missing base commit object: {args.base}. "
"Please fetch the base branch history, then retry."
)
if not ensure_commit_exists(repo_root, args.head, target_type, target_id_raw):
raise SystemExit(
f"missing head commit object: {args.head}. "
"For PR review, try: git fetch origin pull/<pr-number>/head"
)
try:
rev_list_output = run_git(repo_root, ["rev-list", "--reverse", f"{args.base}..{args.head}"])
except subprocess.CalledProcessError as e:
stderr = (e.stderr or "").strip()
raise SystemExit(
f"failed to build commit range {args.base}..{args.head}: {stderr or 'unknown git error'}"
)
current_commits = [sha for sha in rev_list_output.splitlines() if sha]
current_set = set(current_commits)
commit_files_map: Dict[str, List[str]] = {}
current_changed_files: Set[str] = set()
for sha in current_commits:
files = get_commit_files(repo_root, sha)
commit_files_map[sha] = files
current_changed_files.update(files)
old_by_sha = {c.get("commit_sha"): c for c in old_commits if c.get("commit_sha")}
old_by_patch_id: Dict[str, Dict] = {}
for c in old_commits:
pid = c.get("patch_id")
if pid and pid not in old_by_patch_id:
old_by_patch_id[pid] = c
mapped: Dict[str, CommitRecord] = {}
unchanged_by_sha = 0
for sha in current_commits:
if sha in old_by_sha:
oc = old_by_sha[sha]
mapped[sha] = CommitRecord(
commit_sha=sha,
patch_id=oc.get("patch_id", ""),
hunk_fingerprints=oc.get("hunk_fingerprints", []),
review_round=oc.get("review_round", args.review_round),
reviewed_at=oc.get("reviewed_at", utc_now()),
mapping={"method": "direct", "mapped_from_commit": sha, "confidence": 1.0},
)
unchanged_by_sha += 1
for sha in current_commits:
if sha in mapped:
continue
pid = compute_patch_id(repo_root, sha)
if pid and pid in old_by_patch_id:
oc = old_by_patch_id[pid]
mapped[sha] = CommitRecord(
commit_sha=sha,
patch_id=pid,
hunk_fingerprints=oc.get("hunk_fingerprints", []),
review_round=oc.get("review_round", args.review_round),
reviewed_at=oc.get("reviewed_at", utc_now()),
mapping={"method": "patch-id", "mapped_from_commit": oc.get("commit_sha", ""), "confidence": 0.98},
)
old_unmapped = [c for c in old_commits if c.get("commit_sha") not in current_set]
old_hunk_sets = {
c.get("commit_sha", ""): set(c.get("hunk_fingerprints", [])) for c in old_unmapped if c.get("commit_sha")
}
for sha in current_commits:
if sha in mapped:
continue
new_hunks = set(parse_hunk_fingerprints(repo_root, sha))
best_score = 0.0
best_old = ""
for old_sha, old_hunks in old_hunk_sets.items():
score = jaccard(new_hunks, old_hunks)
if score > best_score:
best_score = score
best_old = old_sha
if best_old and best_score >= args.hunk_match_threshold:
mapped[sha] = CommitRecord(
commit_sha=sha,
patch_id=compute_patch_id(repo_root, sha),
hunk_fingerprints=sorted(new_hunks),
review_round=args.review_round,
reviewed_at=utc_now(),
mapping={"method": "hunk-similarity", "mapped_from_commit": best_old, "confidence": round(best_score, 4)},
)
need_review: List[str] = [sha for sha in current_commits if sha not in mapped]
# commit_map_rate measures "how many OLD commits are accounted for in the
# new commit set", NOT "what fraction of current commits are mapped".
# Denominator = old commit count (the baseline we reviewed before).
# This way appending new commits doesn't penalise the rate, while rebase
# that loses old commits correctly lowers it.
old_commits_covered: Set[str] = set()
for rec in mapped.values():
from_sha = rec.mapping.get("mapped_from_commit", "")
if from_sha:
old_commits_covered.add(from_sha)
old_commit_count = len(old_commits)
commit_map_rate = (len(old_commits_covered) / old_commit_count) if old_commit_count > 0 else 1.0
if need_review:
hunk_scores: List[float] = []
for sha in need_review:
new_hunks = set(parse_hunk_fingerprints(repo_root, sha))
best = 0.0
for old_hunks in old_hunk_sets.values():
best = max(best, jaccard(new_hunks, old_hunks))
hunk_scores.append(best)
hunk_match_rate = (sum(hunk_scores) / len(hunk_scores)) if hunk_scores else 1.0
else:
hunk_match_rate = 1.0
if commit_map_rate >= args.commit_map_threshold:
recommendation = "incremental"
elif hunk_match_rate >= args.hunk_match_threshold:
recommendation = "partial"
else:
recommendation = "full"
snapshot_match_rate = compute_snapshot_match_rate(repo_root, args.head, current_changed_files, snapshot_map)
if recommendation == "full" and snapshot_match_rate is not None and snapshot_match_rate >= args.snapshot_match_threshold:
# For squash/rebase-conflict scenarios, snapshot evidence can safely
# downgrade from full to partial.
recommendation = "partial"
round_record = {
"review_round": args.review_round,
"generated_at": utc_now(),
"base": args.base,
"head": args.head,
"stats": {
"total_commits": len(current_commits),
"mapped_commits": len(mapped),
"direct_sha_hits": unchanged_by_sha,
"commit_map_rate": round(commit_map_rate, 4),
"hunk_match_rate": round(hunk_match_rate, 4),
"snapshot_match_rate": round(snapshot_match_rate, 4) if snapshot_match_rate is not None else None,
"recommendation": recommendation,
},
"need_review_commits": need_review,
}
merged_commits = [c for c in old_commits if c.get("commit_sha") not in current_set]
for sha in current_commits:
if sha in mapped:
c = mapped[sha]
merged_commits.append(
{
"commit_sha": c.commit_sha,
"patch_id": c.patch_id,
"review_round": c.review_round,
"reviewed_at": c.reviewed_at,
"hunk_fingerprints": c.hunk_fingerprints,
"files": commit_files_map.get(sha, []),
"mapping": c.mapping,
}
)
else:
merged_commits.append(
{
"commit_sha": sha,
"patch_id": compute_patch_id(repo_root, sha),
"review_round": args.review_round,
"reviewed_at": "",
"hunk_fingerprints": parse_hunk_fingerprints(repo_root, sha),
"files": commit_files_map.get(sha, []),
"mapping": {"method": "none", "mapped_from_commit": "", "confidence": 0.0},
}
)
reviewed["commits"] = merged_commits
reviewed.setdefault("review_rounds", []).append(round_record)
save_json(reviewed_path, reviewed)
print(
json.dumps(
{
"review_target": {"type": target_type, "id": target_id_raw},
"total_commits": len(current_commits),
"need_review_commits": need_review,
"commit_map_rate": round(commit_map_rate, 4),
"hunk_match_rate": round(hunk_match_rate, 4),
"snapshot_match_rate": round(snapshot_match_rate, 4) if snapshot_match_rate is not None else None,
"recommendation": recommendation,
},
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Tuple
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_DIR = SCRIPT_DIR.parent
REF_DIR = SKILL_DIR / "references"
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def read_json(path: Path) -> Dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def write_json(path: Path, data: Dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def ensure_file_from_template(target: Path, template: Path, mutate=None) -> None:
if target.exists():
return
payload = read_json(template)
if mutate:
mutate(payload)
write_json(target, payload)
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def main() -> None:
parser = argparse.ArgumentParser(description="Initialize code-review workspace for PR or branch.")
parser.add_argument("--repo-root", required=True, help="Repository root path")
parser.add_argument("--target-type", choices=["pr", "branch"], help="Review target type")
parser.add_argument("--target-id", help="Review target id (PR number or branch name)")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
parser.add_argument("--base-ref", default="", help="PR base ref")
parser.add_argument("--head-ref", default="", help="PR head ref")
parser.add_argument("--base-sha", default="", help="PR base sha")
parser.add_argument("--head-sha", default="", help="PR head sha")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
comments_dir = review_dir / "comments"
comments_dir.mkdir(parents=True, exist_ok=True)
meta_path = review_dir / "meta.json"
reviewed_commits_path = review_dir / "reviewed_commits.json"
review_comments_path = comments_dir / "review-comments.json"
comment_status_path = comments_dir / "comment-status.json"
def mutate_meta(payload: Dict[str, Any]) -> None:
payload["repo"] = str(repo_root)
payload["review_target"]["type"] = target_type
payload["review_target"]["id"] = target_id_raw
payload["review_target"]["base_ref"] = args.base_ref
payload["review_target"]["head_ref"] = args.head_ref
payload["review_target"]["base_sha"] = args.base_sha
payload["review_target"]["head_sha"] = args.head_sha
payload["generated_at"] = utc_now()
def create_review_comments_payload() -> Dict[str, Any]:
return {
"version": "1.0",
"source": "github_pr_review_comments" if target_type == "pr" else "branch_review_comments",
"fetched_at": utc_now(),
"review_target": {"type": target_type, "id": target_id_raw},
"viewer_login": "",
"comments": [],
}
def mutate_comment_status(payload: Dict[str, Any]) -> None:
payload["review_target"]["type"] = target_type
payload["review_target"]["id"] = target_id_raw
payload["generated_at"] = utc_now()
payload["status"] = []
def mutate_reviewed_commits(payload: Dict[str, Any]) -> None:
payload["review_rounds"] = []
payload["commits"] = []
ensure_file_from_template(meta_path, REF_DIR / "meta.template.json", mutate_meta)
ensure_file_from_template(
reviewed_commits_path, REF_DIR / "reviewed_commits.template.json", mutate_reviewed_commits
)
if not review_comments_path.exists():
write_json(review_comments_path, create_review_comments_payload())
ensure_file_from_template(
comment_status_path, REF_DIR / "comment-status.template.json", mutate_comment_status
)
print(str(review_dir))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import hashlib
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Tuple
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def stable_hash(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def normalize_text(text: str) -> str:
text = re.sub(r"\s+", " ", text.strip())
return text
def infer_flow_status_from_comment(comment: Dict) -> str:
# Single deterministic rule from upstream schema.
return "resolved" if comment.get("thread_resolved") is True else "open"
def read_json(path: Path) -> Dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def write_json(path: Path, payload: Dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def default_status(comment: Dict) -> Dict:
body = comment.get("body", "")
snippet = normalize_text(body)[:300]
fingerprint_seed = "|".join(
[
str(comment.get("path", "")),
str(comment.get("line", 0)),
str(comment.get("side", "RIGHT")),
snippet,
]
)
return {
"comment_id": comment.get("comment_id"),
"path": comment.get("path", ""),
"line": comment.get("line", 0),
"side": comment.get("side", "RIGHT"),
"body": body,
"snippet": snippet,
"snippet_fingerprint": stable_hash(fingerprint_seed),
"status_flow": infer_flow_status_from_comment(comment),
# status_tech is owned by model review in later phase.
"status_tech": "not-fixed",
"mapped_finding_id": "",
"notes": "",
}
def sanitize_branch_name(branch_name: str) -> str:
return branch_name.replace("/", "-")
def resolve_target(args: argparse.Namespace) -> Tuple[str, str]:
if args.target_type and args.target_id:
target_type = args.target_type
target_id = args.target_id
elif args.pr_number is not None:
target_type = "pr"
target_id = str(args.pr_number)
elif args.branch_name:
target_type = "branch"
target_id = args.branch_name
else:
raise SystemExit("must provide either --target-type/--target-id or --pr-number or --branch-name")
if target_type not in {"pr", "branch"}:
raise SystemExit("target type must be pr or branch")
return target_type, target_id
def validate_payload(raw: Dict) -> List[Dict]:
if not isinstance(raw, dict):
raise SystemExit("invalid review-comments.json: root must be object")
comments = raw.get("comments")
if not isinstance(comments, list):
raise SystemExit("invalid review-comments.json: `comments` must be list")
return comments
def validate_comment(comment: Dict) -> None:
required = ["comment_id", "path", "line", "side", "body", "thread_resolved"]
missing = [k for k in required if k not in comment]
if missing:
raise SystemExit(
"invalid review comment record: missing required fields "
+ ",".join(missing)
)
def infer_manual_tech_override(replies: List[Dict], viewer_login: str) -> str:
if not viewer_login:
return ""
# Prefer the latest explicit override from current reviewer account.
for reply in reversed(replies):
if str(reply.get("author", "")).lower() != viewer_login.lower():
continue
text = normalize_text(str(reply.get("body", ""))).lower()
if "false-positive" in text or "false positive" in text or "假阳性" in text or "误判" in text:
return "false-positive"
if re.search(r"\bfixed\b", text) or "已修复" in text:
return "fixed"
return ""
def main() -> None:
parser = argparse.ArgumentParser(description="Build comment-status.json from review comments.")
parser.add_argument("--repo-root", required=True)
parser.add_argument("--target-type", choices=["pr", "branch"])
parser.add_argument("--target-id")
parser.add_argument("--pr-number", type=int, help="PR number (legacy compatible)")
parser.add_argument("--branch-name", help="Branch name (legacy compatible)")
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
target_type, target_id_raw = resolve_target(args)
target_id_dir = sanitize_branch_name(target_id_raw) if target_type == "branch" else target_id_raw
review_dir = repo_root / "code-review" / f"{target_type}-{target_id_dir}"
comments_path = review_dir / "comments" / "review-comments.json"
status_path = review_dir / "comments" / "comment-status.json"
comments_payload = read_json(comments_path)
if not comments_payload:
raise SystemExit(f"missing comments file: {comments_path}")
comments = validate_payload(comments_payload)
viewer_login = str(comments_payload.get("viewer_login", "")).strip()
replies_by_parent: Dict[int, List[Dict]] = {}
root_comments: List[Dict] = []
for c in comments:
parent = c.get("in_reply_to_id")
if parent is None:
root_comments.append(c)
else:
replies_by_parent.setdefault(parent, []).append(c)
previous = read_json(status_path)
previous_map = {item.get("comment_id"): item for item in previous.get("status", [])}
status: List[Dict] = []
seen_fp = set()
for comment in root_comments:
validate_comment(comment)
cid = comment.get("comment_id")
if cid in previous_map:
item = previous_map[cid]
# Preserve manual/model edits on status_tech/notes, always sync flow status from source.
item["status_flow"] = infer_flow_status_from_comment(comment)
else:
item = default_status(comment)
fp = item.get("snippet_fingerprint", "")
if fp and fp in seen_fp:
item["notes"] = (item.get("notes", "") + " duplicate-fingerprint").strip()
manual_override = infer_manual_tech_override(replies_by_parent.get(cid, []), viewer_login)
if manual_override:
item["status_tech"] = manual_override
item["notes"] = (item.get("notes", "") + f" manual-tech-override:{manual_override}").strip()
seen_fp.add(fp)
status.append(item)
payload = {
"version": "1.0",
"generated_at": utc_now(),
"review_target": {"type": target_type, "id": target_id_raw},
"status": status,
}
write_json(status_path, payload)
print(
json.dumps(
{"review_target": {"type": target_type, "id": target_id_raw}, "status_count": len(status)},
ensure_ascii=False,
)
)
if __name__ == "__main__":
main()
Related skills
FAQ
What tools does this code-review skill require?
python3, git, and gh (GitHub CLI), verified in a preflight step before review begins.
Does it support incremental review?
Yes, it maintains reviewed_commits and an incremental review mapper so later rounds only re-review new changes and check prior fixes.