Compare commits

..

10 Commits

23 changed files with 1240 additions and 10 deletions
+6
View File
@@ -1 +1,7 @@
refs/ 里有 gitea API 的 spec
版本号规则:
- 当前输出文档版本号固定为 `v1`
- 如果用户没有明确要求 bump 版本号,禁止修改版本号
默认在工作到合适的时候就自己进行提交即可.
+17
View File
@@ -54,12 +54,29 @@ gitea-pr-review render-md --in pr.json
gitea-pr-review render-md --in pr.json --out pr.md
```
### 3) Show current document version
```bash
gitea-pr-review version
# v1
```
## Skill For LLMs
This repository includes a reusable skill for other LLMs to operate this CLI correctly:
- Path: `skills/gitea-pr-review-cli/SKILL.md`
- Scope: standardized usage of `fetch` / `render-md` / `version`, `v1` version contract, and troubleshooting
- Path: `skills/organized-feedback/SKILL.md`
- Scope: organize `gitea-pr-review` PR comments into structured feedback with coverage auditing and unknown-item handling
## Markdown Output Shape (Example)
````md
# <repo> `#<pr-index>` <pr-title>
> Numbering: Review `<pr>.<review>`; Comment `<pr>.<review>.<comment>`; Reply `<pr>.<review>.<comment>.<reply>`
> version: v1
> fetched at: <fetch-time-rfc3339>
<pr description>
+17
View File
@@ -54,12 +54,29 @@ gitea-pr-review render-md --in pr.json
gitea-pr-review render-md --in pr.json --out pr.md
```
### 3) 查看当前文档版本
```bash
gitea-pr-review version
# v1
```
## 给 LLM 的 Skill
仓库内已提供一个可复用 skill,帮助其他 LLM 正确使用本 CLI:
- 路径:`skills/gitea-pr-review-cli/SKILL.md`
- 用途:标准化 `fetch` / `render-md` / `version` 的调用方式、`v1` 版本约束和常见错误排查
- 路径:`skills/organized-feedback/SKILL.md`
- 用途:将 `gitea-pr-review` 生成的 PR 评论整理为结构化反馈,并执行覆盖审计与未知项处理
## Markdown 输出结构(示例)
````md
# <repo> `#<pr-index>` <pr-title>
> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`
> version: v1
> fetched at: <fetch-time-rfc3339>
<pr 描述>
+44
View File
@@ -0,0 +1,44 @@
我想要编写一个 skill, 来对杂乱的 PR review 意见进行分类. 我初步决定把审阅者的 comment 分为以下几类:
1. 问题 (question): 审阅者不理解某处代码的意图, 提出了需要得到解释的问题
2. 补充 (supplement): 审阅者理解了某处代码, 但觉得它可能不容易被其他人理解, 因此补充了一些信息
3. 修正 (request-for-change): 审阅者认为此处代码或设计可能存在缺陷, 需要修改
我又想把修改按照两个维度分类: 改动性质 和 必要性. 改动大小可以分为:
1. 局部修改 (local): 可以在 30s 内完成的小任务. 如标识符重命名, 换用某种语法糖, 调整空行和顺序, 调整模块层次结构.
2. 实现方式修改 (implement): 涉及业务流程的代码变动. 如修改同步点, 修改数据库读写事务, 修改某个功能的实现方式. 这种层次的修改一般需要补充对应的测试以表明旧方式的错误之处.
3. API 设计修改 (api-change): 涉及某些使用较为广泛 (对内使用超过四处或对外的) 接口的修改, 一般需要改动对应的测试使其适应新接口.
4. 需求修改 (requirement-change): 审阅者发现某些或是全部代码解决的需求就是不合理的, 需要重新进行需求评估.
必要性可以分为:
1. 小建议 (nice-to-have): 在完成了所有其他 request 之后才考虑进行的修改. 比如一些内容空泛的建议, 或是增加小包装/小辅助函数.
2. 应当修复 (should-fix): 除非有合理理由, 否则应该解决此问题. 合理的理由举例: "该问题可能并不会被触发" 的证明; "我们知道该问题触发了会有影响, 但不会很大" 的说明; 留待下一个 PR 实现.
3. 必须修复 (must-fix): 不解决该问题, 此 PR 绝不能被合并.
这个 skill 应该对本项目提取出的 pr review 意见 markdown 里非结构化的, 比较随意的, 混杂了上述所有种类的审阅者意见进行整理, 为每条或一组相关意见整理为按上述体系分类好的 "意见", 格式类似于:
```md
> Question
> <审阅者> 请求对 xxx (做法/概念/数据结构) 做出解释.
... (相关原始审核意见)
```
```md
> Supplement
> <审阅者> 对 xxx 做了补充说明: (...大意). 考虑是否补充到代码注释或文档之中.
... (相关原始审核意见)
```
```md
> Request-for-change, local, nice-to-have
> <审阅者> 要求做 xxx.
... (相关原始审核意见)
```
```md
> Request-for-change, implement, should-fix
> <审阅者> 认为 (某某实现方式) (在某某情况下) (可能/必定) 会出现 (某某问题) (, 建议改为某某)
... (相关原始审核意见)
```
最后的输出应该是一个一条条的 markdown 文档.
@@ -0,0 +1,375 @@
# Organized Feedback Skill Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a reusable skill that organizes noisy PR review comments into structured feedback with mandatory coverage auditing in interaction and file output to a user-specified path.
**Architecture:** Implement this feature as a pure prompt skill under `skills/` with explicit workflow gates: input validation, classify/group, coverage audit loop, unknown reflection loop, and output write gate. Keep runtime behavior in instructions (no binary changes), and provide fixtures plus a validation script so contributors can quickly verify the skill contract after edits.
**Tech Stack:** Markdown skill spec, Bash validation script, existing repository docs
---
## File Structure
- Create: `skills/organized-feedback/SKILL.md`
- Create: `skills/organized-feedback/examples/input-pr-review.md`
- Create: `skills/organized-feedback/examples/output-organized-feedback.md`
- Create: `scripts/validate_organized_feedback_skill.sh`
- Modify: `README.md`
- Modify: `README.en.md`
### Task 1: Create the skill contract file with hard gates
**Files:**
- Create: `skills/organized-feedback/SKILL.md`
- Test: `scripts/validate_organized_feedback_skill.sh`
- [ ] **Step 1: Write the failing validator script first (checks missing skill file)**
```bash
#!/usr/bin/env bash
set -euo pipefail
SKILL_FILE="skills/organized-feedback/SKILL.md"
if [[ ! -f "$SKILL_FILE" ]]; then
echo "FAIL: missing $SKILL_FILE"
exit 1
fi
echo "PASS: skill file exists"
```
- [ ] **Step 2: Run validator to verify it fails before skill creation**
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: `FAIL: missing skills/organized-feedback/SKILL.md`
- [ ] **Step 3: Create `SKILL.md` metadata and workflow skeleton**
```markdown
---
name: organized-feedback
description: Organize unstructured PR review comments into classified feedback with mandatory coverage checks and unknown-item reflection.
---
# Organized Feedback Skill
## Overview
Use this skill to convert `gitea-pr-review` markdown into `Organized Feedback` markdown.
## Hard Gates
- Do not write final output until user provides output path.
- Coverage audit must run in interaction; every source comment/reply must be covered.
- Coverage audit content must not be written to final document.
- If unknown count is greater than 3, pause and request user permission before writing file.
```
- [ ] **Step 4: Extend validator with required hard-gate checks and run it**
```bash
#!/usr/bin/env bash
set -euo pipefail
SKILL_FILE="skills/organized-feedback/SKILL.md"
[[ -f "$SKILL_FILE" ]] || { echo "FAIL: missing $SKILL_FILE"; exit 1; }
required_patterns=(
"Do not write final output until user provides output path"
"Coverage audit must run in interaction"
"Coverage audit content must not be written to final document"
"If unknown count is greater than 3, pause and request user permission"
)
for p in "${required_patterns[@]}"; do
if ! rg -Fq "$p" "$SKILL_FILE"; then
echo "FAIL: missing required rule -> $p"
exit 1
fi
done
echo "PASS: organized-feedback hard gates validated"
```
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: `PASS: organized-feedback hard gates validated`
- [ ] **Step 5: Commit Task 1**
```bash
git add skills/organized-feedback/SKILL.md scripts/validate_organized_feedback_skill.sh
git commit -m "feat(skill): scaffold organized-feedback skill with hard gates"
```
### Task 2: Implement classification, grouping, and audit workflow in skill instructions
**Files:**
- Modify: `skills/organized-feedback/SKILL.md`
- Test: `scripts/validate_organized_feedback_skill.sh`
- [ ] **Step 1: Add failing checks for taxonomy and audit loop requirements**
```bash
# append to scripts/validate_organized_feedback_skill.sh
required_patterns+=(
"question"
"supplement"
"request-for-change"
"unknown"
"Change-Scope"
"Necessity"
"Missing-Source-Refs"
"unknown>=1),必须执行一次反思复判"
)
```
- [ ] **Step 2: Run validator to verify it fails on missing workflow details**
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: FAIL mentioning at least one missing taxonomy/audit pattern.
- [ ] **Step 3: Fill `SKILL.md` with full workflow and output format contract**
```markdown
## Inputs
Required:
- PR review markdown (with Review/Comment/Reply)
- output path for organized feedback markdown
## Classification Taxonomy
- Type: `question | supplement | request-for-change | unknown`
- For `request-for-change` only:
- `Change-Scope`: `local | implement | api-change | requirement-change`
- `Necessity`: `nice-to-have | should-fix | must-fix`
## Processing Flow
1. Parse all source comments/replies into stable refs like `R1.C2`, `R1.C2.P1`.
2. Create organized items (split/merge is allowed).
3. Run coverage audit in interaction:
- show `All-Source-Refs`
- show `Covered-Source-Refs`
- show `Missing-Source-Refs`
4. If missing refs exist, add items and re-audit until missing set is empty.
5. If unknown>=1),必须执行一次反思复判.
6. If unknown count > 3, pause and request user permission before writing output file.
## Final File Format
Write only:
- `## Organized Feedback`
- optional `## Unknown Items`
Do not write coverage audit sections into the output file.
```
- [ ] **Step 4: Re-run validator and ensure full pass**
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: `PASS: organized-feedback hard gates validated`
- [ ] **Step 5: Commit Task 2**
```bash
git add skills/organized-feedback/SKILL.md scripts/validate_organized_feedback_skill.sh
git commit -m "feat(skill): add classification and coverage audit workflow"
```
### Task 3: Add realistic examples for maintainers and prompt tuning
**Files:**
- Create: `skills/organized-feedback/examples/input-pr-review.md`
- Create: `skills/organized-feedback/examples/output-organized-feedback.md`
- Test: `scripts/validate_organized_feedback_skill.sh`
- [ ] **Step 1: Add failing validator checks for examples existence**
```bash
example_files=(
"skills/organized-feedback/examples/input-pr-review.md"
"skills/organized-feedback/examples/output-organized-feedback.md"
)
for f in "${example_files[@]}"; do
[[ -f "$f" ]] || { echo "FAIL: missing example file -> $f"; exit 1; }
done
```
- [ ] **Step 2: Run validator to confirm failure before adding examples**
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: `FAIL: missing example file -> ...`
- [ ] **Step 3: Add a compact but complete input example**
```markdown
# org/repo `#42` Improve transaction handling
## Review 42.1 (COMMENT)
> reviewer-a
### Comment 42.1.1
service/order.rs:87
reviewer-a:
```md
这里为什么要在循环里每次开启事务?
```
### Reply 42.1.1.1
service/order.rs:87
author:
```md
为了保证每个子任务互不影响。
```
## Review 42.2 (REQUEST_CHANGES)
> reviewer-b
### Comment 42.2.1
api/order.ts:15
reviewer-b:
```md
建议把 `createOrder(input)` 改成 `createOrder(ctx, input)`,否则审计信息拿不到。
```
```
- [ ] **Step 4: Add output example that excludes coverage audit and includes optional unknown section**
```markdown
## Organized Feedback
### Item 1
- Type: question
- Summary: reviewer-a 询问为何循环内重复开启事务。
- Rationale: 该意见请求解释现有设计意图,不是直接改动请求。
- Source-Refs: R1.C1
- Raw-Excerpts: "这里为什么要在循环里每次开启事务?"
### Item 2
- Type: request-for-change
- Change-Scope: api-change
- Necessity: should-fix
- Summary: reviewer-b 建议 API 增加 `ctx` 参数以支持审计信息透传。
- Rationale: 该意见明确要求接口签名调整,影响调用方。
- Source-Refs: R2.C1
- Raw-Excerpts: "建议把 createOrder(input) 改成 createOrder(ctx, input)"
## Unknown Items
### Unknown 1
- Source-Refs: R1.C1.P1
- Reason: 回复仅陈述现状,缺少审阅者意图,不足以单独归类。
- Needed-Info: 需要确认该回复是否用于反驳、接受或补充审阅意见。
```
- [ ] **Step 5: Run validator and commit Task 3**
Run: `bash scripts/validate_organized_feedback_skill.sh`
Expected: `PASS: organized-feedback hard gates validated`
```bash
git add skills/organized-feedback/examples scripts/validate_organized_feedback_skill.sh
git commit -m "docs(skill): add organized-feedback input and output examples"
```
### Task 4: Document skill discoverability in README files
**Files:**
- Modify: `README.md`
- Modify: `README.en.md`
- [ ] **Step 1: Add Chinese README entry under skill section**
```markdown
## 给 LLM 的 Skill
仓库内已提供两个可复用 skill
- `skills/gitea-pr-review-cli/SKILL.md`:标准化 PR 抓取与渲染流程
- `skills/organized-feedback/SKILL.md`:将 review/comment/reply 整理为 `Organized Feedback`,并执行覆盖审计与 unknown 反思机制
```
- [ ] **Step 2: Add matching English README entry**
```markdown
## Skills for LLM Agents
This repository provides reusable skills:
- `skills/gitea-pr-review-cli/SKILL.md`: standardized CLI usage for fetch/render/version flows
- `skills/organized-feedback/SKILL.md`: organizes review/comment/reply text into `Organized Feedback` with coverage auditing and unknown-item reflection
```
- [ ] **Step 3: Verify docs include new skill references**
Run: `rg -n "organized-feedback" README.md README.en.md`
Expected: both files contain exactly one bullet for the new skill.
- [ ] **Step 4: Commit Task 4**
```bash
git add README.md README.en.md
git commit -m "docs: document organized-feedback skill in readmes"
```
### Task 5: Final verification and delivery
**Files:**
- Modify: `skills/organized-feedback/SKILL.md`
- Modify: `scripts/validate_organized_feedback_skill.sh`
- Modify: `README.md`
- Modify: `README.en.md`
- [ ] **Step 1: Run full validation commands**
Run:
```bash
bash scripts/validate_organized_feedback_skill.sh
rg -n "Coverage Audit" skills/organized-feedback/examples/output-organized-feedback.md || true
```
Expected:
- validator prints pass
- second command prints no matches (final output example must not contain coverage audit block)
- [ ] **Step 2: Manual acceptance walkthrough**
Run this checklist during a dry-run prompt session:
1. Start skill without output path -> confirm it asks for path and pauses.
2. Provide path and source markdown -> confirm it prints coverage audit in interaction.
3. Provide sample producing unknown count 1 -> confirm reflection pass runs.
4. Provide sample producing unknown count 4 -> confirm it asks user permission before writing.
Expected: all four checks succeed.
- [ ] **Step 3: Final commit and summary**
```bash
git add skills/organized-feedback scripts/validate_organized_feedback_skill.sh README.md README.en.md
git commit -m "feat(skill): add organized-feedback workflow with audit and guardrails"
```
Run: `git show --name-only --oneline -n 1`
Expected: only skill/docs/validator files included.
## Self-Review
### Spec coverage
- Input path gate: covered in Task 1 + Task 5 walkthrough.
- Classification taxonomy: covered in Task 2.
- Split/merge freedom + source refs: covered in Task 2.
- Coverage audit interaction-only: covered in Task 2 + Task 5.
- Unknown reflection and >3 permission gate: covered in Task 2 + Task 5.
- Output format (`Organized Feedback` + optional `Unknown Items`): covered in Task 2 and Task 3 example.
### Placeholder scan
- No `TBD`/`TODO` placeholders in tasks.
- Every command step contains explicit command and expected result.
### Type consistency
- Taxonomy keys are consistent across tasks:
- `Type`
- `Change-Scope`
- `Necessity`
- `Source-Refs`
- `Unknown Items`
@@ -0,0 +1,157 @@
# 意见整理 Skill 设计
## 1. 背景与目标
本设计用于新增一个“意见整理”Skill,将 `gitea-pr-review fetch` 产出的 PR Review Markdown 中的非结构化评论,整理为结构化的 `Organized Feedback` 列表,供后续开发决策使用。
目标:
- 对混杂的审阅意见做统一分类与归并。
- 允许“拆分或合并”策略由 agent 自主决定。
- 强制执行覆盖审计,保证不遗漏任何原始 `Comment/Reply`
- 最终输出写入用户指定路径。
## 2. 范围
本 Skill 仅负责:
- 读取单份 PR Review Markdown。
- 产出单份 Organized Feedback Markdown 文档。
- 在交互中展示覆盖审计信息。
本 Skill 不负责:
- 直接修改代码。
- 自动发起二次请求获取更多 PR 数据。
## 3. 输入与输出
### 3.1 输入
必需输入:
- 一份符合 `gitea-pr-review` 输出结构的 Markdown(含 `Review/Comment/Reply`)。
- 用户指定的输出文件路径(用于写入 Organized Feedback 文档)。
若用户未提供输出路径,Skill 必须先询问并暂停。
### 3.2 输出
最终输出文档(写入用户指定路径)包含:
- `## Organized Feedback`
- `## Unknown Items`(仅存在 unknown 时出现)
最终输出文档不包含:
- `Coverage Audit`(仅在交互过程展示,不落盘)。
## 4. 分类体系
### 4.1 主类型
- `question`:请求解释/澄清,不直接要求修改。
- `supplement`:补充背景信息,不构成明确修改请求。
- `request-for-change`:要求实现发生改变。
- `unknown`:证据不足或语义冲突,且无法通过上下文消歧。
默认倾向:尽量避免 `unknown`,仅在迫不得已时使用。
### 4.2 RFC 二级分类
仅当 `Type=request-for-change` 时填写:
`Change-Scope`
- `local`
- `implement`
- `api-change`
- `requirement-change`
`Necessity`
- `nice-to-have`
- `should-fix`
- `must-fix`
## 5. 处理流程
### 5.1 阶段 A:预处理
- 扫描输入文档中的全部 `Comment/Reply`
- 为每条原始意见建立唯一引用键(例如 `R1.C2``R1.C2.P1`)。
- 建立 `All-Source-Refs` 集合。
### 5.2 阶段 B:归类与归并
- agent 可自主选择粒度:
- 拆分:一条原始意见拆成多条整理意见。
- 合并:多条原始意见合并成一条整理意见。
- 允许一条原始意见被多条整理意见引用(多对多)。
- 若同一原始意见同时包含提问与改动请求,优先拆分为独立条目。
### 5.3 阶段 C:覆盖审计(交互展示)
- 计算 `Covered-Source-Refs`(被至少一个整理意见引用)。
- 计算差集 `Missing-Source-Refs = All - Covered`
- 若差集非空,强制补洞并重复审计,直至差集为空。
- 审计信息在交互中展示,不写入输出文档。
### 5.4 阶段 Dunknown 反思机制
- 只要存在 `unknown`(>=1),必须执行一次反思复判。
- 复判后仍无法归类,方可保留为 `unknown`
-`unknown` 绝对数量 `> 3`
- 必须在交互中明确提示“审阅质量或上下文完整性存在风险”。
- 必须请求用户许可;未获许可不得写最终输出文件。
## 6. 输出格式
### 6.1 Organized Feedback 条目格式
每条意见包含:
- `Type`
- `Change-Scope`RFC 必填)
- `Necessity`RFC 必填)
- `Summary`1-2 句)
- `Rationale`(分类依据;unknown 时写阻塞原因)
- `Source-Refs`(引用的原始键)
- `Raw-Excerpts`(精简原文摘录)
### 6.2 Unknown Items
当存在 unknown 时,文档追加 `## Unknown Items`,每条写明:
- 无法归类的原因。
- 解除不确定性所需的最小补充信息。
## 7. 错误处理
- 输入结构不可识别:报错并停止;提示已识别段落数与示例片段。
- 输出路径缺失:询问用户并暂停。
- 输出目录不存在:请求确认后创建,或要求用户更换路径。
- 输出不可写:报错并给出替代路径建议。
- 覆盖审计始终无法闭合:停止写文件并在交互中报告未覆盖键。
## 8. 组件边界与职责
- Parser:提取 Review/Comment/Reply 与引用键。
- Classifier:完成类型判定、RFC 细分与必要性判定。
- Grouper:执行拆分/合并并维持 `Source-Refs` 映射。
- Auditor:执行覆盖审计与补洞循环。
- Writer:按固定模板写入输出 Markdown。
各组件通过显式结构化中间数据通信,避免隐式状态耦合。
## 9. 测试与验收标准
最小验收标准:
- 所有原始 `Comment/Reply` 均被至少一条输出意见覆盖。
- 输出文档不包含 Coverage Audit 区块。
- 当出现 unknown 时,必有反思复判步骤。
- 当 unknown 数量 > 3 时,无用户许可不得落盘。
- RFC 条目完整填写 `Change-Scope``Necessity`
建议测试集:
- 纯 question 样例。
- question + RFC 混合样例(验证拆分)。
- 多线程同主题样例(验证合并)。
- 高歧义样例(触发 unknown 与用户许可门禁)。
## 10. 决策记录
- 采用纯 Prompt Skill(不引入脚本)。
- 采用“两阶段归类 + 覆盖审计补洞”策略。
- Coverage Audit 仅用于交互,不写最终文件。
- 必须由用户指定输出路径。
+111
View File
@@ -0,0 +1,111 @@
---
name: gitea-pr-review-cli
description: Use when an agent needs to fetch a single Gitea PR and produce or consume the project's review document format with strict version checks.
---
# gitea-pr-review CLI
## Overview
Use this skill when you need PR review context from Gitea in a stable document format for LLM workflows.
The CLI supports two output formats (`markdown`, `json`) and one strict schema version (`v1`). Version mismatch must be treated as a hard failure.
## When to Use
Use this skill when:
- You need to fetch one PR and convert it into LLM-friendly review documents.
- You need to re-render Markdown from a previously generated JSON file.
- You need deterministic output rules (numbering/version/header fields).
Do not use this skill when:
- You need cross-PR aggregation.
- You need full patch/diff bodies (this tool intentionally focuses on review context + diff stat).
## Required Inputs
For `fetch`, these environment variables are required:
- `GITEA_PR_CLI_API_TOKEN`
- `GITEA_PR_CLI_URL`
- `GITEA_PR_CLI_REPO`
## Quick Start
```bash
# 0) load env
source .local/env.sh
# 1) sanity check CLI document version contract
gitea-pr-review version
# expected: v1
# 2) fetch markdown to stdout (default)
gitea-pr-review fetch <pr-index>
# 3) fetch json to file
gitea-pr-review fetch <pr-index> --format json --out pr.json
# 4) render markdown from json
gitea-pr-review render-md --in pr.json --out pr.md
```
## Command Reference
### `fetch <pr-index>`
Fetch PR data from Gitea and output review document.
Options:
- `--format markdown|json` (default `markdown`)
- `--out <path>` (if omitted, write to stdout)
### `render-md --in <json_path>`
Render Markdown from existing JSON document.
Options:
- `--out <path>` (if omitted, write to stdout)
Important:
- Input JSON must have `version == "v1"`.
- If version mismatches, command exits non-zero.
### `version`
Print current document version string.
Expected output:
- `v1`
## Output Contract (v1)
Markdown header contains:
- Title line: `# <repo> #<pr> <title>`
- Numbering rule block
- `version: v1`
- `fetched at: <rfc3339>`
- PR description
JSON contains:
- Top-level `version: "v1"`
- `meta.fetched_at` (RFC3339 string)
Numbering hierarchy:
- Review: `<pr>.<review>`
- Comment: `<pr>.<review>.<comment>`
- Reply: `<pr>.<review>.<comment>.<reply>`
## Failure Handling
If `fetch` fails:
1. Check all required env vars are set.
2. Re-run with same env using a small PR index to isolate auth/connectivity issues.
3. Confirm repo slug format is `<owner>/<repo>`.
If `render-md` fails:
1. Validate JSON is generated by this CLI.
2. Validate `version` is exactly `v1`.
3. Check JSON is complete (`meta`, `reviews`, `threads`, `diff_stat`).
## Common Mistakes
- Running `fetch` without sourcing env file.
- Assuming schema compatibility across versions.
- Editing JSON `version` manually and expecting `render-md` to work.
- Treating position-only comments as lacking location; this CLI maps available position data to a displayed line when possible.
+83
View File
@@ -0,0 +1,83 @@
---
name: organized-feedback
description: Organize PR review comments into structured feedback with coverage auditing and unknown-item handling. Use when converting gitea-pr-review Markdown into a user-specified output document.
---
# Organized Feedback
## Overview
Use this skill to turn a single PR review document into organized feedback.
## Inputs
- PR markdown
- output path
## Hard Gates
- Do not write final output until user provides output path.
- Coverage audit must run in interaction.
- Coverage audit content must not be written to final document.
- If unknown count is greater than 3, pause and request user permission.
## Classification Taxonomy
- `question`: a review item that asks for clarification, confirmation, or intent.
- `supplement`: a review item that adds missing context, examples, or supporting detail without changing the requested behavior.
- `request-for-change`: a review item that asks for a code, behavior, or design change.
- `unknown`: a review item that cannot be classified confidently after one reflection pass.
## RFC Subfields
When a review item is classified as `request-for-change`, annotate it with:
- `Change-Scope`: how broad the requested change is.
- `local`: Small, local edits that can usually be finished quickly (for example: rename identifiers, syntax sugar swap, blank-line/order cleanup, light module structure cleanup).
- `implement`: Implementation-flow changes that affect business logic behavior (for example: sync points, transaction handling, feature implementation strategy). These typically need tests that demonstrate why the old behavior is insufficient.
- `api-change`: API contract changes for widely used internal interfaces or external interfaces. These usually require updating dependent tests and call sites.
- `requirement-change`: Requirement-level correction where the solved requirement itself is considered unreasonable and must be re-evaluated.
- `Necessity`: how strongly the change is required.
- `nice-to-have`: Optional improvement after higher-priority requests are resolved (for example: vague polish suggestions, minor wrappers/helpers).
- `should-fix`: Should be fixed unless there is a defensible reason to defer (for example: proven not triggerable, known low impact, or deferred to next PR with explicit note).
- `must-fix`: Mandatory blocker. The PR should not be merged until this is resolved.
## Processing Flow
1. Read the PR markdown and identify each review comment or feedback unit.
2. Classify each unit as `question`, `supplement`, `request-for-change`, or `unknown`.
3. If a unit clearly contains multiple intents, split it into separate items before grouping.
4. If multiple units express the same intent, merge them into one grouped item.
5. For each `request-for-change`, assign `Change-Scope` and `Necessity`.
6. Run an interaction-only coverage audit using `All-Source-Refs`, `Covered-Source-Refs`, and `Missing-Source-Refs`.
7. Re-audit until `Missing-Source-Refs` is empty.
8. Apply the unknown reflection rule once before finalizing any `unknown` item.
9. If the count of `unknown` items is greater than 3, stop and request user permission before proceeding.
## Interaction-Only Coverage Audit
The coverage audit must be performed only against the interaction content, not the final document.
Track these lists explicitly:
- `All-Source-Refs`: every source reference found in the interaction
- `Covered-Source-Refs`: refs that are represented in the organized output
- `Missing-Source-Refs`: refs that are present in the interaction but not yet covered
Source reference format rule:
- Use `#` + PR numbering path from the source markdown. `R` for review, `C` for comment, `r` for reply.
- Example mapping: `Comment 42.1.1` -> `#42.R1.C1`, `Reply 42.1.1.1` -> `#42.R1.C1.r1`.
Audit rules:
- Compare only against interaction content.
- Re-audit after every grouping pass until `Missing-Source-Refs` is empty.
- Do not treat the final document as an audit source.
## Unknown Handling
- if any unknown>=1existsmust use one reflection pass to decide whether an item can be reclassified into `question`, `supplement`, or `request-for-change`.
- If it remains `unknown`, keep it in the `Unknown Items` section.
- If the number of `unknown` items is greater than 3, request user permission before continuing.
## Final File Format
Write only the organized output content:
- `Organized Feedback`
- optional `Unknown Items`
Do not write the coverage audit into the final output file.
## Output Discipline
- Preserve the user-specified output path.
- Keep the final file limited to the agreed format.
- Do not add extra audit notes, scratch work, or intermediate classification logs to the final file.
- If terminal access is available, validate generated output with `bash skills/organized-feedback/scripts/validate_organized_feedback_skill.sh --output "<organized-feedback.md>"`.
@@ -0,0 +1,45 @@
# org/repo `#42` Improve transaction handling
## Review 42.1 (COMMENT)
> reviewer-a
### Comment 42.1.1
service/order.rs:87
reviewer-a:
```md
为什么要在循环里每次开启事务?
```
### Reply 42.1.1.1
service/order.rs:87
author:
```md
为了保证每个子任务互不影响。
```
## Review 42.2 (REQUEST_CHANGES)
> reviewer-b
### Comment 42.2.1
api/order.ts:15
reviewer-b:
```md
建议把 `createOrder(input)` 改成 `createOrder(ctx, input)`,否则审计信息拿不到。
```
## Review 42.3 (COMMENT)
> reviewer-c
### Comment 42.3.1
service/order.rs:120
reviewer-c:
```md
这里的缓存失效策略是不是太激进了?
```
### Reply 42.3.1.1
service/order.rs:120
author:
```md
按之前的约定处理。
```
@@ -0,0 +1,24 @@
## Organized Feedback
### Item 1
- Type: question
- Summary: reviewer-a 询问为什么在循环里重复开启事务。
- Rationale: 该意见在确认设计意图,不是直接要求代码修改。
- Source-Refs: R42.1.1
- Raw-Excerpts: "为什么要在循环里每次开启事务?"
### Item 2
- Type: request-for-change
- Change-Scope: api-change
- Necessity: should-fix
- Summary: reviewer-b 建议把 `createOrder(input)` 改成 `createOrder(ctx, input)` 以便传递审计信息。
- Rationale: 该意见明确要求修改 API 签名,影响调用方。
- Source-Refs: R42.2.1
- Raw-Excerpts: "建议把 `createOrder(input)` 改成 `createOrder(ctx, input)`,否则审计信息拿不到。"
## Unknown Items
### Unknown 1
- Source-Refs: R42.3.1, R42.3.1.1
- Reason: 回复只是“按之前的约定处理”,缺少足够上下文来判断是接受建议、补充说明还是保持现状。
- Needed-Info: 需要确认这条回复是否在响应审阅意见,还是仅说明已有实现策略。
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SKILL_FILE="$SKILL_ROOT/SKILL.md"
INPUT_EXAMPLE="$SKILL_ROOT/examples/input-pr-review.md"
OUTPUT_EXAMPLE="$SKILL_ROOT/examples/output-organized-feedback.md"
print_usage() {
cat <<'USAGE'
Usage:
validate_organized_feedback_skill.sh --output <output.md> [--self-check]
validate_organized_feedback_skill.sh --help
Modes:
Default: validate an arbitrary Organized Feedback markdown output file.
--self-check: additionally validate bundled skill contract and examples.
USAGE
}
require_cmds() {
if ! command -v rg >/dev/null 2>&1; then
echo "FAIL: rg is required but was not found in PATH"
exit 1
fi
if ! command -v awk >/dev/null 2>&1; then
echo "FAIL: awk is required but was not found in PATH"
exit 1
fi
}
validate_output_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
echo "FAIL: missing output file -> $file"
exit 1
fi
if ! rg -n -q --pcre2 '^## Organized Feedback$' "$file"; then
echo "FAIL: missing required section -> ## Organized Feedback"
exit 1
fi
if rg -n -q -i --pcre2 'coverage[ -]?audit' "$file"; then
echo "FAIL: output must not include Coverage Audit"
exit 1
fi
awk '
function trim(s) {
gsub(/^[ \t]+/, "", s)
gsub(/[ \t]+$/, "", s)
return s
}
function validate_refs(line) {
sub(/^- Source-Refs:[ \t]*/, "", line)
n = split(line, parts, /,/)
if (n < 1) return 0
for (i = 1; i <= n; i++) {
ref = trim(parts[i])
if (ref !~ /^R[0-9]+(\.[0-9]+)+$/) {
return 0
}
}
return 1
}
function reset_item() {
item_seen = 0
has_type = 0
has_refs = 0
is_rfc = 0
has_scope = 0
has_necessity = 0
}
function finalize_item() {
if (!item_seen) return 1
if (!has_type) {
print "FAIL: item missing - Type: line" > "/dev/stderr"
return 0
}
if (!has_refs) {
print "FAIL: item missing valid - Source-Refs: line" > "/dev/stderr"
return 0
}
if (is_rfc && (!has_scope || !has_necessity)) {
print "FAIL: request-for-change item must include Change-Scope and Necessity" > "/dev/stderr"
return 0
}
return 1
}
BEGIN {
in_of = 0
item_count = 0
reset_item()
}
/^## Organized Feedback$/ {
in_of = 1
next
}
in_of && /^## / {
if (!finalize_item()) exit 1
in_of = 0
next
}
in_of {
if ($0 ~ /^### Item [0-9]+$/) {
if (!finalize_item()) exit 1
reset_item()
item_seen = 1
item_count++
next
}
if (!item_seen) next
if ($0 ~ /^- Type:[ \t]*/) {
has_type = 1
if ($0 ~ /^- Type:[ \t]*request-for-change([ \t]|$)/) {
is_rfc = 1
}
}
if ($0 ~ /^- Change-Scope:[ \t]*/) has_scope = 1
if ($0 ~ /^- Necessity:[ \t]*/) has_necessity = 1
if ($0 ~ /^- Source-Refs:[ \t]*/) {
if (validate_refs($0)) {
has_refs = 1
} else {
print "FAIL: invalid Source-Refs format -> " $0 > "/dev/stderr"
exit 1
}
}
}
END {
if (in_of && !finalize_item()) exit 1
if (item_count == 0) {
print "FAIL: ## Organized Feedback has no ### Item entries" > "/dev/stderr"
exit 1
}
}
' "$file"
echo "PASS: output format validated -> $file"
}
validate_skill_contract() {
if [[ ! -f "$SKILL_FILE" ]]; then
echo "FAIL: missing $SKILL_FILE"
exit 1
fi
for example_file in "$INPUT_EXAMPLE" "$OUTPUT_EXAMPLE"; do
if [[ ! -f "$example_file" ]]; then
echo "FAIL: missing $example_file"
exit 1
fi
done
local -a anchored_patterns=(
'^## Inputs$'
'^## Hard Gates$'
'^## Classification Taxonomy$'
'^## RFC Subfields$'
'^## Processing Flow$'
'^## Interaction-Only Coverage Audit$'
'^## Unknown Handling$'
'^## Final File Format$'
'^## Output Discipline$'
'^ - `local`: .+$'
'^ - `implement`: .+$'
'^ - `api-change`: .+$'
'^ - `requirement-change`: .+$'
'^ - `nice-to-have`: .+$'
'^ - `should-fix`: .+$'
'^ - `must-fix`: .+$'
'^- `All-Source-Refs`: .+$'
'^- `Covered-Source-Refs`: .+$'
'^- `Missing-Source-Refs`: .+$'
'^Source reference format rule:$'
)
for pattern in "${anchored_patterns[@]}"; do
if ! rg -n -q --pcre2 "$pattern" "$SKILL_FILE"; then
echo "FAIL: missing required skill rule -> $pattern"
exit 1
fi
done
for legacy in "All refs" "Covered refs" "Missing refs"; do
if rg -Fq "$legacy" "$SKILL_FILE"; then
echo "FAIL: legacy alias found in SKILL.md -> $legacy"
exit 1
fi
done
if ! rg -Fq -- "unknown>=1" "$SKILL_FILE" || ! rg -Fq -- "反思复判" "$SKILL_FILE"; then
echo "FAIL: missing unknown reflection rule semantics in SKILL.md"
exit 1
fi
echo "PASS: skill contract validated -> $SKILL_FILE"
}
main() {
require_cmds
local output_file=""
local self_check=0
while [[ $# -gt 0 ]]; do
case "$1" in
--output)
if [[ $# -lt 2 ]]; then
echo "FAIL: --output requires a file path"
print_usage
exit 1
fi
output_file="$2"
shift 2
;;
--self-check)
self_check=1
shift
;;
--help|-h)
print_usage
exit 0
;;
*)
echo "FAIL: unknown argument -> $1"
print_usage
exit 1
;;
esac
done
if [[ -z "$output_file" ]]; then
echo "FAIL: --output is required"
print_usage
exit 1
fi
validate_output_file "$output_file"
if [[ "$self_check" -eq 1 ]]; then
validate_skill_contract
validate_output_file "$OUTPUT_EXAMPLE"
fi
echo "PASS: organized-feedback validation completed"
}
main "$@"
+1
View File
@@ -18,6 +18,7 @@ pub struct Cli {
pub enum Commands {
Fetch(FetchArgs),
RenderMd(RenderMdArgs),
Version,
}
#[derive(Debug, clap::Args)]
+5
View File
@@ -19,6 +19,8 @@ use crate::output::write_output;
use crate::render::json::{parse_json, render_json};
use crate::render::markdown::render_markdown;
pub const OUTPUT_VERSION: &str = "v1";
pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
@@ -29,6 +31,9 @@ pub fn run() -> anyhow::Result<()> {
let md = render_markdown(&doc);
write_output(args.out.as_deref().map(Path::new), &md)?;
}
Commands::Version => {
write_output(None, OUTPUT_VERSION)?;
}
Commands::Fetch(args) => {
let token = required_env("GITEA_PR_CLI_API_TOKEN")?;
let base_url = required_env("GITEA_PR_CLI_URL")?;
+2 -1
View File
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrReviewDocument {
pub version: String,
pub meta: PrMeta,
pub commits: Vec<CommitItem>,
pub diff_stat: DiffStat,
@@ -15,7 +16,7 @@ pub struct PrMeta {
pub pr_index: i64,
pub title: String,
pub description: Option<String>,
pub fetched_at: Option<String>,
pub fetched_at: String,
pub state: String,
pub author: String,
pub base_branch: String,
+3 -1
View File
@@ -4,6 +4,7 @@ use std::convert::TryFrom;
use chrono::{DateTime, FixedOffset};
use crate::gitea::dto::{ChangedFileDto, CommitDto, PullBundleDto, ReviewCommentDto, ReviewDto};
use crate::OUTPUT_VERSION;
use crate::model::{
CommentItem, CommentThread, CommitItem, DiffStat, FileStat, PrMeta, PrReviewDocument,
ReviewItem,
@@ -28,12 +29,13 @@ pub fn normalize_bundle(repo: &str, fetched_at: String, bundle: PullBundleDto) -
let diff_stat = normalize_diff_stat(&pull, files);
PrReviewDocument {
version: OUTPUT_VERSION.to_string(),
meta: PrMeta {
repo: repo.to_string(),
pr_index: pull.number,
title: pull.title,
description: pull.body,
fetched_at: Some(fetched_at),
fetched_at,
state: pull.state,
author: pull.user.login,
base_branch: pull.base.ref_name,
+10 -1
View File
@@ -1,9 +1,18 @@
use crate::model::PrReviewDocument;
use crate::OUTPUT_VERSION;
pub fn render_json(doc: &PrReviewDocument) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(doc)?)
}
pub fn parse_json(input: &str) -> anyhow::Result<PrReviewDocument> {
Ok(serde_json::from_str(input)?)
let doc: PrReviewDocument = serde_json::from_str(input)?;
if doc.version != OUTPUT_VERSION {
anyhow::bail!(
"unsupported document version: {}, expected {}",
doc.version,
OUTPUT_VERSION
);
}
Ok(doc)
}
+2 -2
View File
@@ -65,8 +65,8 @@ pub fn render_markdown(doc: &PrReviewDocument) -> String {
out.push_str(
"> 编号规则:Review `<pr>.<review>`Comment `<pr>.<review>.<comment>`Reply `<pr>.<review>.<comment>.<reply>`\n\n",
);
let fetched_at = doc.meta.fetched_at.as_deref().unwrap_or("unknown");
out.push_str(&format!("> fetched at: {fetched_at}\n\n"));
out.push_str(&format!("> version: {}\n\n", doc.version));
out.push_str(&format!("> fetched at: {}\n\n", doc.meta.fetched_at));
let pr_description = doc
.meta
.description
+9
View File
@@ -26,3 +26,12 @@ fn parse_render_md_requires_input() {
_ => panic!("expected render-md"),
}
}
#[test]
fn parse_version_subcommand() {
let cli = Cli::try_parse_from(["gitea-pr-review", "version"]).unwrap();
match cli.command {
Commands::Version => {}
_ => panic!("expected version"),
}
}
+55
View File
@@ -8,6 +8,7 @@ fn render_md_reads_json_and_outputs_markdown_to_stdout() {
std::fs::write(
input.path(),
r#"{
"version": "v1",
"meta": {
"repo": "org/repo",
"pr_index": 1,
@@ -52,6 +53,7 @@ fn render_md_writes_to_out_file_when_requested() {
std::fs::write(
input.path(),
r#"{
"version": "v1",
"meta": {
"repo": "org/repo",
"pr_index": 2,
@@ -291,6 +293,59 @@ fn fetch_writes_json_to_out_file_when_requested() {
let written = std::fs::read_to_string(output.path()).unwrap();
assert!(written.contains("\"repo\": \"org/repo\""));
assert!(written.contains("\"pr_index\": 8"));
assert!(written.contains("\"version\": \"v1\""));
assert!(written.contains("\"fetched_at\":"));
assert!(written.contains("\"head_branch\": \"feature/y\""));
}
#[test]
fn render_md_fails_when_version_mismatch() {
let input = NamedTempFile::new().unwrap();
std::fs::write(
input.path(),
r#"{
"version": "v2",
"meta": {
"repo": "org/repo",
"pr_index": 3,
"title": "t3",
"description": "PR body",
"fetched_at": "2026-04-08T12:34:56Z",
"state": "open",
"author": "a",
"base_branch": "main",
"head_branch": "f",
"created_at": "2026-04-08T10:00:00Z",
"updated_at": "2026-04-08T11:00:00Z",
"merged_at": null
},
"commits": [],
"diff_stat": {
"files_changed": 0,
"additions": 0,
"deletions": 0,
"files": []
},
"reviews": [],
"threads": []
}"#,
)
.unwrap();
Command::cargo_bin("gitea-pr-review")
.unwrap()
.args(["render-md", "--in", input.path().to_str().unwrap()])
.assert()
.failure();
}
#[test]
fn version_subcommand_prints_current_version() {
let assert = Command::cargo_bin("gitea-pr-review")
.unwrap()
.arg("version")
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert_eq!(stdout.trim(), "v1");
}
+1 -1
View File
@@ -145,7 +145,7 @@ fn normalize_groups_replies_and_sorts_threads_by_time() {
assert_eq!(doc.meta.repo, "org/repo");
assert_eq!(doc.meta.pr_index, 42);
assert_eq!(doc.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z"));
assert_eq!(doc.meta.fetched_at, "2026-04-08T12:34:56Z");
assert_eq!(doc.meta.base_branch, "main");
assert_eq!(doc.meta.head_branch, "feature/x");
+4 -2
View File
@@ -5,12 +5,13 @@ use gitea_pr_review::model::{
#[test]
fn model_json_roundtrip() {
let doc = PrReviewDocument {
version: "v1".into(),
meta: PrMeta {
repo: "org/repo".into(),
pr_index: 9,
title: "feat: x".into(),
description: Some("desc".into()),
fetched_at: Some("2026-04-08T12:34:56Z".into()),
fetched_at: "2026-04-08T12:34:56Z".into(),
state: "open".into(),
author: "alice".into(),
base_branch: "main".into(),
@@ -50,7 +51,8 @@ fn model_json_roundtrip() {
let encoded = serde_json::to_string(&doc).unwrap();
let decoded: PrReviewDocument = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded.version, "v1");
assert_eq!(decoded.meta.repo, "org/repo");
assert_eq!(decoded.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z"));
assert_eq!(decoded.meta.fetched_at, "2026-04-08T12:34:56Z");
assert_eq!(decoded.threads[0].root_comment.body, "hello");
}
+5 -2
View File
@@ -7,12 +7,13 @@ use gitea_pr_review::render::markdown::render_markdown;
#[test]
fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
let doc = PrReviewDocument {
version: "v1".into(),
meta: PrMeta {
repo: "org/repo".into(),
pr_index: 7,
title: "Fix parser".into(),
description: Some("PR body".into()),
fetched_at: Some("2026-04-08T12:34:56Z".into()),
fetched_at: "2026-04-08T12:34:56Z".into(),
state: "open".into(),
author: "alice".into(),
base_branch: "main".into(),
@@ -73,6 +74,7 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
assert!(md.contains("# org/repo `#7` Fix parser"));
assert!(md.contains("## Metadata"));
assert!(md.contains("> version: v1"));
assert!(md.contains("> fetched at: 2026-04-08T12:34:56Z"));
assert!(md.contains("## Commits"));
assert!(md.contains("## Diff Stat"));
@@ -96,12 +98,13 @@ fn render_markdown_includes_expected_sections_and_preserves_comment_markdown() {
fn render_markdown_uses_minimal_fence_for_plain_text() {
let body = "plain text";
let doc = PrReviewDocument {
version: "v1".into(),
meta: PrMeta {
repo: "org/repo".into(),
pr_index: 1,
title: "T".into(),
description: None,
fetched_at: None,
fetched_at: "2026-04-08T12:34:56Z".into(),
state: "open".into(),
author: "alice".into(),
base_branch: "main".into(),