@ranger1/dx 0.1.90 → 0.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/cli/commands/core.js +92 -0
- package/lib/cli/help.js +2 -1
- package/lib/codex-initial.js +19 -215
- package/package.json +2 -2
- package/skills/backend-layering-audit-fixer/SKILL.md +180 -0
- package/{codex/skills → skills}/doctor/SKILL.md +2 -9
- package/{codex/skills → skills}/doctor/scripts/doctor.sh +2 -253
- package/skills/git-pr-ship/SKILL.md +481 -0
- package/skills/naming-audit-fixer/SKILL.md +149 -0
- package/skills/naming-audit-fixer/references/fix-guide.md +93 -0
- package/skills/naming-audit-fixer/scripts/audit_naming.py +534 -0
- package/codex/agents/fixer.toml +0 -37
- package/codex/agents/orchestrator.toml +0 -11
- package/codex/agents/reviewer.toml +0 -52
- package/codex/agents/spark.toml +0 -18
- package/codex/skills/pr-review-loop/SKILL.md +0 -209
- package/codex/skills/pr-review-loop/agents/openai.yaml +0 -4
- package/codex/skills/pr-review-loop/references/agents/pr-context.md +0 -73
- package/codex/skills/pr-review-loop/references/agents/pr-precheck.md +0 -161
- package/codex/skills/pr-review-loop/references/agents/pr-review-aggregate.md +0 -188
- package/codex/skills/pr-review-loop/references/skill-layout.md +0 -25
- package/codex/skills/pr-review-loop/scripts/gh_review_harvest.py +0 -292
- package/codex/skills/pr-review-loop/scripts/pr_context.py +0 -351
- package/codex/skills/pr-review-loop/scripts/pr_review_aggregate.py +0 -951
- package/codex/skills/pr-review-loop/scripts/test_pr_review_aggregate.py +0 -876
- package/codex/skills/pr-review-loop/scripts/test_validate_reviewer_prompts.py +0 -92
- package/codex/skills/pr-review-loop/scripts/validate_reviewer_prompts.py +0 -87
- /package/{codex/skills → skills}/doctor/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/scripts/e2e_e2e_audit.py +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/scripts/env_accessor_audit.py +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/error-handling-standard.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/foundation-bootstrap.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/scripts/error_handling_audit.py +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/SKILL.md +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-release/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-release/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/online-debug-guard/SKILL.md +0 -0
- /package/{codex/skills → skills}/online-debug-guard/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/references/pagination-standard.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +0 -0
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: aggregate PR reviews + create fix file
|
|
3
|
-
mode: subagent
|
|
4
|
-
model: openai/gpt-5.3-codex
|
|
5
|
-
temperature: 0.1
|
|
6
|
-
tools:
|
|
7
|
-
bash: true
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# PR Review Aggregator
|
|
11
|
-
|
|
12
|
-
## Cache 约定(强制)
|
|
13
|
-
|
|
14
|
-
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
15
|
-
|
|
16
|
-
## 输入(两种模式)
|
|
17
|
-
|
|
18
|
-
### 模式 A:评审聚合 + 生成 fixFile + 发布评审评论
|
|
19
|
-
|
|
20
|
-
- `PR #<number>`
|
|
21
|
-
- `round: <number>`
|
|
22
|
-
- `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
|
|
23
|
-
- `contextFile: <path>`(例如:`./.cache/pr-context-...md`)
|
|
24
|
-
- `reviewFile: <path>`(多行,1+ 条;例如:`./.cache/review-...md`)
|
|
25
|
-
|
|
26
|
-
### 模式 B:发布修复评论(基于 fixReportFile)
|
|
27
|
-
|
|
28
|
-
- `PR #<number>`
|
|
29
|
-
- `round: <number>`
|
|
30
|
-
- `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
|
|
31
|
-
- `fixReportFile: <path>`(例如:`./.cache/fix-report-...md`)
|
|
32
|
-
|
|
33
|
-
示例:
|
|
34
|
-
|
|
35
|
-
```text
|
|
36
|
-
PR #123
|
|
37
|
-
round: 1
|
|
38
|
-
runId: 123-1-a1b2c3d
|
|
39
|
-
contextFile: ./.cache/pr-context-pr123-r1-123-1-a1b2c3d.md
|
|
40
|
-
reviewFile: ./.cache/review-SEC-pr123-r1-123-1-a1b2c3d.md
|
|
41
|
-
reviewFile: ./.cache/review-PERF-pr123-r1-123-1-a1b2c3d.md
|
|
42
|
-
reviewFile: ./.cache/review-MAINT-pr123-r1-123-1-a1b2c3d.md
|
|
43
|
-
reviewFile: ./.cache/review-BIZ-pr123-r1-123-1-a1b2c3d.md
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## 执行方式(强制)
|
|
47
|
-
|
|
48
|
-
所有确定性工作(发评论/生成 fixFile/输出 JSON)都由 `${CODEX_HOME:-$HOME/.codex}/skills/pr-review-loop/scripts/pr_review_aggregate.py` 完成。
|
|
49
|
-
|
|
50
|
-
你只做三件事:
|
|
51
|
-
|
|
52
|
-
1) 在模式 A 里读取 `contextFile`、所有 `reviewFile`,由大模型做最终语义裁决。
|
|
53
|
-
2) 产出一份**结构化聚合结果 JSON**,再把它作为参数传给脚本(不落盘)。
|
|
54
|
-
3) 调用脚本后,把脚本 stdout 的 JSON **原样返回**给调用者(不做解释/分析)。
|
|
55
|
-
|
|
56
|
-
## 最终裁决边界(强制)
|
|
57
|
-
|
|
58
|
-
- “是否存在问题”“哪些问题必须修”“是否可以 stop” 都由你基于 `reviewFile` 语义判断。
|
|
59
|
-
- 脚本**不再**根据 reviewer 文本自行推断 `P0/P1`、`stop` 或“有没有问题”。
|
|
60
|
-
- 如果 reviewer 文本里缺少关键信息,导致你无法可靠裁决,必须返回错误;禁止把不确定性伪装成 `stop=true`。
|
|
61
|
-
|
|
62
|
-
## 聚合结果 JSON(模式 A 必须生成)
|
|
63
|
-
|
|
64
|
-
你需要基于 `contextFile`、所有 `reviewFile`,先完成这些判断:
|
|
65
|
-
|
|
66
|
-
- 去重:本质相同的问题只保留一条
|
|
67
|
-
- decision-log 过滤:已 fixed 的问题过滤;已 rejected 的问题仅在满足升级质疑条件时保留
|
|
68
|
-
- 分级:把保留的问题分成 `mustFixFindings` 和 `optionalFindings`
|
|
69
|
-
- 终止判断:仅当你确认没有 `mustFixFindings` 时,才可令 `stop=true`
|
|
70
|
-
|
|
71
|
-
然后输出一行 JSON,结构固定如下:
|
|
72
|
-
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"stop": false,
|
|
76
|
-
"mustFixFindings": [
|
|
77
|
-
{
|
|
78
|
-
"id": "SEC-001",
|
|
79
|
-
"priority": "P1",
|
|
80
|
-
"category": "bug",
|
|
81
|
-
"file": "apps/api/src/service.ts",
|
|
82
|
-
"line": "10",
|
|
83
|
-
"title": "标题",
|
|
84
|
-
"description": "描述",
|
|
85
|
-
"suggestion": "建议"
|
|
86
|
-
}
|
|
87
|
-
],
|
|
88
|
-
"optionalFindings": [
|
|
89
|
-
{
|
|
90
|
-
"id": "STY-002",
|
|
91
|
-
"priority": "P3",
|
|
92
|
-
"category": "quality",
|
|
93
|
-
"file": "apps/web/src/page.tsx",
|
|
94
|
-
"line": "22",
|
|
95
|
-
"title": "标题",
|
|
96
|
-
"description": "描述",
|
|
97
|
-
"suggestion": "建议"
|
|
98
|
-
}
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
强约束:
|
|
104
|
-
|
|
105
|
-
- 必须是一行 JSON,不要代码块,不要解释。
|
|
106
|
-
- `stop=true` 时,`mustFixFindings` 必须为空数组。
|
|
107
|
-
- `stop=false` 时,`mustFixFindings` 必须至少有一条。
|
|
108
|
-
- 每个 finding 必须包含这些字段且非空:`id`、`priority`、`category`、`file`、`line`、`title`、`description`、`suggestion`
|
|
109
|
-
- `priority` 只能是 `P0` / `P1` / `P2` / `P3`
|
|
110
|
-
- `mustFixFindings` 只允许 `P0` / `P1`
|
|
111
|
-
- `optionalFindings` 只允许 `P2` / `P3`
|
|
112
|
-
|
|
113
|
-
## 重复分组与 decision-log(仅作为你的思考步骤)
|
|
114
|
-
|
|
115
|
-
你仍然需要基于所有 `reviewFile` 内容判断重复问题和 decision-log 匹配,但这些中间结果**不再**单独传给脚本;它们只体现在最终的聚合结果 JSON 里。
|
|
116
|
-
|
|
117
|
-
## 智能匹配(仅在模式 A + decision-log 存在时)
|
|
118
|
-
|
|
119
|
-
如果 decision-log(`./.cache/decision-log-pr<PR_NUMBER>.md`)存在,你需要基于 LLM 判断每个新 finding 与已决策问题的本质是否相同,并把判断结果体现在最终聚合结果里。
|
|
120
|
-
|
|
121
|
-
**匹配原则**:
|
|
122
|
-
- **Essence 匹配**:对比 `essence` 字段与新 finding 的问题本质。
|
|
123
|
-
- **文件强绑定**:仅当 decision-log 条目的 `file` 与新 finding 的 `file` **完全一致**时才进行匹配。
|
|
124
|
-
- 若文件被重命名/删除/拆分,视为不同问题(为了稳定性,不处理复杂的 rename 映射)。
|
|
125
|
-
- 若 decision-log 条目缺少 `file` 字段(旧数据),则跳过匹配(视为不相关)。
|
|
126
|
-
|
|
127
|
-
**流程**:
|
|
128
|
-
|
|
129
|
-
1. 读取 decision-log,提取已 rejected 问题的 `essence` 和 `file` 字段
|
|
130
|
-
2. 逐个新 finding,**先检查 file 是否匹配**
|
|
131
|
-
- 若 file 不匹配 → 视为 New Issue
|
|
132
|
-
- 若 file 匹配 → 继续对比 essence
|
|
133
|
-
3. 若 essence 也匹配("问题本质相同"):
|
|
134
|
-
4. 收集可升级的问题(重新质疑阈值):
|
|
135
|
-
- **升级阈值**:优先级差距 ≥ 2 级
|
|
136
|
-
- 例如:已 rejected P3 but finding 为 P1 → 可升级质疑
|
|
137
|
-
- 例如:已 rejected P2 but finding 为 P0 → 可升级质疑
|
|
138
|
-
- 例如:已 rejected P2 but finding 为 P1 → 不升级(仅差 1 级)
|
|
139
|
-
5. 只把满足条件的升级问题保留到最终 `mustFixFindings` 或 `optionalFindings`;其余继续按 rejected 过滤。
|
|
140
|
-
|
|
141
|
-
## 调用脚本(强制)
|
|
142
|
-
|
|
143
|
-
模式 A(带 reviewFile + 聚合结果):
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
python3 "${CODEX_HOME:-$HOME/.codex}/skills/pr-review-loop/scripts/pr_review_aggregate.py" \
|
|
147
|
-
--pr <PR_NUMBER> \
|
|
148
|
-
--round <ROUND> \
|
|
149
|
-
--run-id <RUN_ID> \
|
|
150
|
-
--context-file <CONTEXT_FILE> \
|
|
151
|
-
--review-file <REVIEW_FILE_1> \
|
|
152
|
-
--review-file <REVIEW_FILE_2> \
|
|
153
|
-
--review-file <REVIEW_FILE_3> \
|
|
154
|
-
--aggregate-result-b64 <BASE64_JSON>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**参数说明**:
|
|
158
|
-
|
|
159
|
-
- `--aggregate-result-b64`:base64 编码的聚合结果 JSON
|
|
160
|
-
|
|
161
|
-
模式 B(带 fixReportFile):
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
python3 "${CODEX_HOME:-$HOME/.codex}/skills/pr-review-loop/scripts/pr_review_aggregate.py" \
|
|
165
|
-
--pr <PR_NUMBER> \
|
|
166
|
-
--round <ROUND> \
|
|
167
|
-
--run-id <RUN_ID> \
|
|
168
|
-
--fix-report-file <FIX_REPORT_FILE>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## 脚本输出处理(强制)
|
|
172
|
-
|
|
173
|
-
- 脚本 stdout 只会输出**单一一行 JSON**(可 `JSON.parse()`)。
|
|
174
|
-
- **成功时**:你的最终输出必须是**脚本 stdout 的那一行 JSON 原样内容**。
|
|
175
|
-
- 典型返回:`{"stop":true}` 或 `{"stop":false,"fixFile":"..."}` 或 `{"ok":true}`
|
|
176
|
-
- 禁止:解释/分析/补充文字
|
|
177
|
-
- 禁止:代码块(```)
|
|
178
|
-
- 禁止:前后空行
|
|
179
|
-
- **失败/异常时**:
|
|
180
|
-
- 若脚本 stdout 已输出合法 JSON(包含 `error` 或其他字段)→ 仍然**原样返回该 JSON**。
|
|
181
|
-
- 若脚本未输出合法 JSON / 退出异常 → 仅返回一行 JSON:`{"error":"PR_REVIEW_AGGREGATE_AGENT_FAILED"}`(必要时可加 `detail` 字段)。
|
|
182
|
-
|
|
183
|
-
## fixFile 结构(补充说明)
|
|
184
|
-
|
|
185
|
-
脚本在模式 A 下根据你提供的聚合结果生成 fixFile,分为两段:
|
|
186
|
-
|
|
187
|
-
- `## IssuesToFix`:只包含 P0/P1(必须修)
|
|
188
|
-
- `## OptionalIssues`:包含 P2/P3(由 fixer 自主决定是否修复,或拒绝并说明原因)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# pr-review-loop 技能目录说明
|
|
2
|
-
|
|
3
|
-
本目录用于在当前仓库内运行 PR 审核闭环,不依赖历史外置目录。
|
|
4
|
-
|
|
5
|
-
## 目录结构
|
|
6
|
-
|
|
7
|
-
- `SKILL.md`:技能入口 + 主编排(唯一真值源)
|
|
8
|
-
- `agents/openai.yaml`:技能 UI 元数据
|
|
9
|
-
- `references/agents/*.md`:子代理输入输出契约
|
|
10
|
-
- `scripts/*.py`:确定性脚本(context/harvest/aggregate)
|
|
11
|
-
|
|
12
|
-
## 快速验证
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
python3 -m pytest -q "${CODEX_HOME:-$HOME/.codex}/skills/pr-review-loop/scripts/test_pr_review_aggregate.py"
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## 核心约束
|
|
19
|
-
|
|
20
|
-
- 缓存统一使用 `./.cache/`
|
|
21
|
-
- `runId` 必须透传,禁止下游重算
|
|
22
|
-
- reviewer 可并行,其它步骤严格串行
|
|
23
|
-
- 修复阶段必须调用 `fixer`,编排器不得直接修代码
|
|
24
|
-
- 错误处理采用“分级重试优先”,不是“见 error 立刻终止”
|
|
25
|
-
- 需要联网步骤的角色显式配置 `network_access = true`
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
# Deterministic GitHub PR review harvester.
|
|
4
|
-
#
|
|
5
|
-
# - Fetches inline review threads via GraphQL (reviewThreads) with pagination.
|
|
6
|
-
# - Fetches PR reviews and PR issue comments via REST (gh api) with pagination.
|
|
7
|
-
# - Writes a raw JSON file into project cache: ./.cache/
|
|
8
|
-
# - Prints exactly one JSON object to stdout: {"rawFile":"./.cache/...json"}
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
from datetime import datetime, timezone
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
MARKER_SUBSTR = "<!-- pr-review-loop-marker"
|
|
20
|
-
|
|
21
|
-
def _repo_root():
|
|
22
|
-
try:
|
|
23
|
-
p = subprocess.run(
|
|
24
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
25
|
-
stdout=subprocess.PIPE,
|
|
26
|
-
stderr=subprocess.DEVNULL,
|
|
27
|
-
text=True,
|
|
28
|
-
)
|
|
29
|
-
out = (p.stdout or "").strip()
|
|
30
|
-
if p.returncode == 0 and out:
|
|
31
|
-
return Path(out)
|
|
32
|
-
except Exception:
|
|
33
|
-
pass
|
|
34
|
-
return Path.cwd()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _cache_dir(repo_root):
|
|
38
|
-
return (repo_root / ".cache").resolve()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _repo_relpath(repo_root, p):
|
|
42
|
-
try:
|
|
43
|
-
rel = p.resolve().relative_to(repo_root.resolve())
|
|
44
|
-
return "./" + rel.as_posix()
|
|
45
|
-
except Exception:
|
|
46
|
-
return os.path.basename(str(p))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
REPO_ROOT = _repo_root()
|
|
50
|
-
CACHE_DIR = _cache_dir(REPO_ROOT)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _json_out(obj):
|
|
54
|
-
sys.stdout.write(json.dumps(obj, ensure_ascii=True))
|
|
55
|
-
sys.stdout.write("\n")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _run_capture(cmd):
|
|
59
|
-
try:
|
|
60
|
-
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
61
|
-
return p.returncode, p.stdout, p.stderr
|
|
62
|
-
except FileNotFoundError as e:
|
|
63
|
-
return 127, "", str(e)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _has_loop_marker(text):
|
|
67
|
-
if not text:
|
|
68
|
-
return False
|
|
69
|
-
try:
|
|
70
|
-
return MARKER_SUBSTR in str(text)
|
|
71
|
-
except Exception:
|
|
72
|
-
return False
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _require_gh_auth():
|
|
76
|
-
rc, out, err = _run_capture(["gh", "auth", "status"])
|
|
77
|
-
if rc == 127:
|
|
78
|
-
return False, "GH_CLI_NOT_FOUND", "gh not found in PATH"
|
|
79
|
-
if rc != 0:
|
|
80
|
-
detail = (err or out or "").strip()
|
|
81
|
-
if len(detail) > 4000:
|
|
82
|
-
detail = detail[-4000:]
|
|
83
|
-
return False, "GH_NOT_AUTHENTICATED", detail
|
|
84
|
-
return True, None, None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _resolve_owner_repo(explicit_repo):
|
|
88
|
-
if explicit_repo:
|
|
89
|
-
s = str(explicit_repo).strip()
|
|
90
|
-
if s and "/" in s:
|
|
91
|
-
return s
|
|
92
|
-
rc, out, _ = _run_capture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
|
|
93
|
-
owner_repo = out.strip() if rc == 0 else ""
|
|
94
|
-
return owner_repo or None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _gh_api_json(args):
|
|
98
|
-
rc, out, err = _run_capture(["gh", "api"] + args)
|
|
99
|
-
if rc != 0:
|
|
100
|
-
raise RuntimeError(f"GH_API_FAILED: {(err or out or '').strip()}")
|
|
101
|
-
try:
|
|
102
|
-
return json.loads(out or "null")
|
|
103
|
-
except Exception:
|
|
104
|
-
raise RuntimeError("GH_API_JSON_PARSE_FAILED")
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _gh_api_graphql(query, variables):
|
|
108
|
-
cmd = ["gh", "api", "graphql", "-f", f"query={query}"]
|
|
109
|
-
for k, v in (variables or {}).items():
|
|
110
|
-
if isinstance(v, int):
|
|
111
|
-
cmd.extend(["-F", f"{k}={v}"])
|
|
112
|
-
elif v is None:
|
|
113
|
-
cmd.extend(["-f", f"{k}="])
|
|
114
|
-
else:
|
|
115
|
-
cmd.extend(["-f", f"{k}={v}"])
|
|
116
|
-
|
|
117
|
-
rc, out, err = _run_capture(cmd)
|
|
118
|
-
if rc != 0:
|
|
119
|
-
raise RuntimeError(f"GH_GRAPHQL_FAILED: {(err or out or '').strip()}")
|
|
120
|
-
try:
|
|
121
|
-
return json.loads(out or "null")
|
|
122
|
-
except Exception:
|
|
123
|
-
raise RuntimeError("GH_GRAPHQL_JSON_PARSE_FAILED")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _flatten_threads(gql_data):
|
|
127
|
-
threads = []
|
|
128
|
-
pr = (((gql_data or {}).get("data") or {}).get("repository") or {}).get("pullRequest") or {}
|
|
129
|
-
conn = pr.get("reviewThreads") or {}
|
|
130
|
-
nodes = conn.get("nodes") or []
|
|
131
|
-
for t in nodes:
|
|
132
|
-
is_resolved = bool((t or {}).get("isResolved"))
|
|
133
|
-
is_outdated = bool((t or {}).get("isOutdated"))
|
|
134
|
-
if is_resolved or is_outdated:
|
|
135
|
-
continue
|
|
136
|
-
comments_conn = (t or {}).get("comments") or {}
|
|
137
|
-
comments_nodes = comments_conn.get("nodes") or []
|
|
138
|
-
comments = []
|
|
139
|
-
for c in comments_nodes:
|
|
140
|
-
body = (c or {}).get("body") or ""
|
|
141
|
-
body_text = (c or {}).get("bodyText") or ""
|
|
142
|
-
if _has_loop_marker(body) or _has_loop_marker(body_text):
|
|
143
|
-
continue
|
|
144
|
-
author = (c or {}).get("author") or {}
|
|
145
|
-
comments.append(
|
|
146
|
-
{
|
|
147
|
-
"id": (c or {}).get("id"),
|
|
148
|
-
"databaseId": (c or {}).get("databaseId"),
|
|
149
|
-
"url": (c or {}).get("url"),
|
|
150
|
-
"author": {
|
|
151
|
-
"login": author.get("login"),
|
|
152
|
-
"type": author.get("__typename"),
|
|
153
|
-
},
|
|
154
|
-
"body": body,
|
|
155
|
-
"bodyText": body_text,
|
|
156
|
-
"createdAt": (c or {}).get("createdAt"),
|
|
157
|
-
"updatedAt": (c or {}).get("updatedAt"),
|
|
158
|
-
}
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
if not comments:
|
|
162
|
-
continue
|
|
163
|
-
threads.append(
|
|
164
|
-
{
|
|
165
|
-
"id": (t or {}).get("id"),
|
|
166
|
-
"isResolved": False,
|
|
167
|
-
"isOutdated": False,
|
|
168
|
-
"path": (t or {}).get("path"),
|
|
169
|
-
"line": (t or {}).get("line"),
|
|
170
|
-
"originalLine": (t or {}).get("originalLine"),
|
|
171
|
-
"startLine": (t or {}).get("startLine"),
|
|
172
|
-
"originalStartLine": (t or {}).get("originalStartLine"),
|
|
173
|
-
"comments": comments,
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
page_info = conn.get("pageInfo") or {}
|
|
178
|
-
return threads, {
|
|
179
|
-
"hasNextPage": bool(page_info.get("hasNextPage")),
|
|
180
|
-
"endCursor": page_info.get("endCursor"),
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _fetch_all_review_threads(owner, repo, pr_number):
|
|
185
|
-
query = (
|
|
186
|
-
"query($owner:String!,$repo:String!,$prNumber:Int!,$after:String){"
|
|
187
|
-
"repository(owner:$owner,name:$repo){"
|
|
188
|
-
"pullRequest(number:$prNumber){"
|
|
189
|
-
"reviewThreads(first:100,after:$after){"
|
|
190
|
-
"pageInfo{hasNextPage endCursor}"
|
|
191
|
-
"nodes{"
|
|
192
|
-
"id isResolved isOutdated path line originalLine startLine originalStartLine "
|
|
193
|
-
"comments(first:100){nodes{"
|
|
194
|
-
"id databaseId url body bodyText createdAt updatedAt author{login __typename}"
|
|
195
|
-
"}}"
|
|
196
|
-
"}"
|
|
197
|
-
"}"
|
|
198
|
-
"}"
|
|
199
|
-
"}"
|
|
200
|
-
"}"
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
after = None
|
|
204
|
-
all_threads = []
|
|
205
|
-
while True:
|
|
206
|
-
data = _gh_api_graphql(query, {"owner": owner, "repo": repo, "prNumber": pr_number, "after": after})
|
|
207
|
-
threads, page = _flatten_threads(data)
|
|
208
|
-
all_threads.extend(threads)
|
|
209
|
-
if not page.get("hasNextPage"):
|
|
210
|
-
break
|
|
211
|
-
after = page.get("endCursor")
|
|
212
|
-
if not after:
|
|
213
|
-
break
|
|
214
|
-
return all_threads
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def main(argv):
|
|
218
|
-
class _ArgParser(argparse.ArgumentParser):
|
|
219
|
-
def error(self, message):
|
|
220
|
-
raise ValueError(message)
|
|
221
|
-
|
|
222
|
-
parser = _ArgParser(add_help=False)
|
|
223
|
-
parser.add_argument("--pr", type=int, required=True)
|
|
224
|
-
parser.add_argument("--round", type=int, default=1)
|
|
225
|
-
parser.add_argument("--run-id", required=True)
|
|
226
|
-
parser.add_argument("--repo")
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
args = parser.parse_args(argv)
|
|
230
|
-
except ValueError:
|
|
231
|
-
_json_out({"error": "INVALID_ARGS"})
|
|
232
|
-
return 2
|
|
233
|
-
|
|
234
|
-
ok, code, detail = _require_gh_auth()
|
|
235
|
-
if not ok:
|
|
236
|
-
_json_out({"error": code, "detail": detail})
|
|
237
|
-
return 1
|
|
238
|
-
|
|
239
|
-
owner_repo = _resolve_owner_repo(args.repo)
|
|
240
|
-
if not owner_repo:
|
|
241
|
-
_json_out({"error": "REPO_NOT_FOUND"})
|
|
242
|
-
return 1
|
|
243
|
-
if "/" not in owner_repo:
|
|
244
|
-
_json_out({"error": "INVALID_REPO"})
|
|
245
|
-
return 1
|
|
246
|
-
|
|
247
|
-
owner, repo = owner_repo.split("/", 1)
|
|
248
|
-
pr_number = int(args.pr)
|
|
249
|
-
round_num = int(args.round)
|
|
250
|
-
run_id = str(args.run_id).strip()
|
|
251
|
-
if not run_id:
|
|
252
|
-
_json_out({"error": "MISSING_RUN_ID"})
|
|
253
|
-
return 1
|
|
254
|
-
|
|
255
|
-
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
256
|
-
raw_basename = f"gh-review-raw-pr{pr_number}-r{round_num}-{run_id}.json"
|
|
257
|
-
raw_path = CACHE_DIR / raw_basename
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
threads = _fetch_all_review_threads(owner, repo, pr_number)
|
|
261
|
-
|
|
262
|
-
reviews = _gh_api_json([f"repos/{owner_repo}/pulls/{pr_number}/reviews", "--paginate"])
|
|
263
|
-
issue_comments = _gh_api_json([f"repos/{owner_repo}/issues/{pr_number}/comments", "--paginate"])
|
|
264
|
-
|
|
265
|
-
if isinstance(reviews, list):
|
|
266
|
-
reviews = [r for r in reviews if not _has_loop_marker((r or {}).get("body") or "")]
|
|
267
|
-
if isinstance(issue_comments, list):
|
|
268
|
-
issue_comments = [c for c in issue_comments if not _has_loop_marker((c or {}).get("body") or "")]
|
|
269
|
-
|
|
270
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
271
|
-
payload = {
|
|
272
|
-
"repo": owner_repo,
|
|
273
|
-
"pr": pr_number,
|
|
274
|
-
"round": round_num,
|
|
275
|
-
"runId": run_id,
|
|
276
|
-
"generatedAt": now,
|
|
277
|
-
"reviewThreads": threads,
|
|
278
|
-
"reviews": reviews if isinstance(reviews, list) else [],
|
|
279
|
-
"issueComments": issue_comments if isinstance(issue_comments, list) else [],
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
raw_path.write_text(json.dumps(payload, ensure_ascii=True), encoding="utf-8", newline="\n")
|
|
283
|
-
except Exception as e:
|
|
284
|
-
_json_out({"error": "HARVEST_FAILED", "detail": str(e)[:800]})
|
|
285
|
-
return 1
|
|
286
|
-
|
|
287
|
-
_json_out({"rawFile": _repo_relpath(REPO_ROOT, raw_path)})
|
|
288
|
-
return 0
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if __name__ == "__main__":
|
|
292
|
-
raise SystemExit(main(sys.argv[1:]))
|