@ranger1/dx 0.1.39 → 0.1.41
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/@opencode/agents/__pycache__/pr_review_aggregate.cpython-314.pyc +0 -0
- package/@opencode/agents/__pycache__/test_pr_review_aggregate.cpython-314-pytest-9.0.2.pyc +0 -0
- package/@opencode/agents/claude-reviewer.md +15 -0
- package/@opencode/agents/codex-reviewer.md +15 -0
- package/@opencode/agents/gemini-reviewer.md +15 -0
- package/@opencode/agents/gh-thread-reviewer.md +15 -0
- package/@opencode/agents/pr-fix.md +63 -1
- package/@opencode/agents/pr-review-aggregate.md +45 -2
- package/@opencode/agents/pr_review_aggregate.py +243 -5
- package/@opencode/agents/test_pr_review_aggregate.py +500 -0
- package/@opencode/commands/doctor.md +10 -146
- package/@opencode/commands/oh_attach.json +92 -0
- package/@opencode/commands/opencode_attach.json +26 -0
- package/@opencode/commands/opencode_attach.py +142 -0
- package/@opencode/commands/pr-review-loop.md +16 -4
- package/README.md +57 -0
- package/lib/opencode-initial.js +16 -5
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ pnpm add -g @ranger1/dx@latest && dx initial
|
|
|
18
18
|
|
|
19
19
|
## Step 1: 并行检测
|
|
20
20
|
|
|
21
|
-
**同时执行以下
|
|
21
|
+
**同时执行以下 4 个 Bash 调用(真正并行):**
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# 批次 1: CLI 版本检测
|
|
@@ -32,24 +32,20 @@ echo "agent-browser:" && (which agent-browser && agent-browser --version 2>/dev/
|
|
|
32
32
|
# 批次 2: 项目文件检测
|
|
33
33
|
echo "=== PROJECT_FILES ===";
|
|
34
34
|
echo "AGENTS.md:" && (test -f AGENTS.md && echo "FOUND" || echo "NOT_FOUND");
|
|
35
|
-
echo "instructions:" && (grep -q '"AGENTS.md"' ~/.config/opencode/opencode.json 2>/dev/null && grep -q '"ruler/' ~/.config/opencode/opencode.json 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED");
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
38
|
# 批次 3: OpenCode 插件检测
|
|
40
39
|
# 注意:插件名可能带版本号(如 @1.3.0),使用模糊匹配
|
|
41
40
|
echo "=== OPENCODE_PLUGINS ===";
|
|
42
|
-
echo "oh-my-opencode:" && (grep -q 'oh-my-opencode'
|
|
43
|
-
echo "opencode-openai-codex-auth:" && (grep -q 'opencode-openai-codex-auth'
|
|
41
|
+
echo "oh-my-opencode:" && (opencode plugin list 2>/dev/null | grep -q 'oh-my-opencode' && echo "INSTALLED" || echo "NOT_INSTALLED");
|
|
42
|
+
echo "opencode-openai-codex-auth:" && (opencode plugin list 2>/dev/null | grep -q 'opencode-openai-codex-auth' && echo "INSTALLED" || echo "NOT_INSTALLED");
|
|
44
43
|
```
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
|
-
# 批次 4:
|
|
48
|
-
echo "===
|
|
49
|
-
echo "
|
|
50
|
-
echo "agents.sisyphus.variant:" && (node -e "const fs=require('node:fs');const os=require('node:os');const p=os.homedir()+'/.config/opencode/oh-my-opencode.json';try{const j=JSON.parse(fs.readFileSync(p,'utf8'));process.exit(j?.agents?.sisyphus?.variant==='none'?0:1)}catch(e){process.exit(1)}" 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED");
|
|
51
|
-
echo "agent.quick:" && (grep -Eq '"agent"[[:space:]]*:' ~/.config/opencode/opencode.json 2>/dev/null && grep -Eq '"quick"[[:space:]]*:' ~/.config/opencode/opencode.json 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED");
|
|
52
|
-
echo "agent.middle:" && (grep -Eq '"agent"[[:space:]]*:' ~/.config/opencode/opencode.json 2>/dev/null && grep -Eq '"middle"[[:space:]]*:' ~/.config/opencode/opencode.json 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED");
|
|
46
|
+
# 批次 4: attach 配置(统一)
|
|
47
|
+
echo "=== OPENCODE_ATTACH ===";
|
|
48
|
+
echo "attach:" && (python3 ~/.opencode/commands/opencode_attach.py --dry-run >/dev/null 2>&1 && echo "READY" || echo "NOT_READY");
|
|
53
49
|
```
|
|
54
50
|
|
|
55
51
|
```bash
|
|
@@ -72,14 +68,10 @@ dx | <状态> | <版本>
|
|
|
72
68
|
python3 | <状态> | <版本>
|
|
73
69
|
python(软链接) | <状态> | <版本>
|
|
74
70
|
AGENTS.md | <状态> | -
|
|
75
|
-
全局 instructions 配置 | <状态> | -
|
|
76
71
|
oh-my-opencode 插件 | <状态> | -
|
|
77
72
|
opencode-openai-codex-auth 插件 | <状态> | -
|
|
78
73
|
agent-browser | <状态> | <版本>
|
|
79
|
-
|
|
80
|
-
全局 agents.sisyphus.variant 配置 | <状态> | -
|
|
81
|
-
全局 agent.quick 配置 | <状态> | -
|
|
82
|
-
全局 agent.middle 配置 | <状态> | -
|
|
74
|
+
attach(全局配置写入) | <状态> | -
|
|
83
75
|
```
|
|
84
76
|
|
|
85
77
|
---
|
|
@@ -107,62 +99,6 @@ brew install opencode || npm install -g opencode
|
|
|
107
99
|
|
|
108
100
|
- AGENTS.md 文件不存在,OpenCode 需要此文件作为项目指令入口
|
|
109
101
|
- 建议创建或检查文件路径
|
|
110
|
-
- AGENTS.md 应位于项目根目录,并在全局 `~/.config/opencode/opencode.json` 的 `instructions` 中引用
|
|
111
|
-
|
|
112
|
-
### 3.3 全局 opencode.json instructions 配置缺失
|
|
113
|
-
|
|
114
|
-
**注意:instructions 配置应在全局配置文件 `~/.config/opencode/opencode.json` 中,而非项目根目录。项目根目录不需要 opencode.json 文件。**
|
|
115
|
-
|
|
116
|
-
1. 先读取现有全局配置:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
cat ~/.config/opencode/opencode.json
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
2. 使用 Edit 工具在 `~/.config/opencode/opencode.json` 中添加或修改 `instructions` 配置:
|
|
123
|
-
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"$schema": "https://opencode.ai/config.json",
|
|
127
|
-
"instructions": ["AGENTS.md", "ruler/**/*.md"]
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### 3.4 全局配置指令无效
|
|
132
|
-
|
|
133
|
-
使用 Edit 工具修复 `~/.config/opencode/opencode.json`,确保包含:
|
|
134
|
-
|
|
135
|
-
- `"AGENTS.md"`: 主配置文件
|
|
136
|
-
- `"ruler/**/*.md"`: 自动加载 ruler 目录下所有 .md 文件(因 OpenCode 不支持 @ 引用)
|
|
137
|
-
|
|
138
|
-
### 3.5 全局 OpenCode 插件安装
|
|
139
|
-
|
|
140
|
-
**注意:OpenCode 插件配置应在全局配置文件 `~/.config/opencode/opencode.json` 中,而非项目根目录。**
|
|
141
|
-
|
|
142
|
-
1. 先读取现有全局配置:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
cat ~/.config/opencode/opencode.json
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
2. 使用 Edit 工具在 `~/.config/opencode/opencode.json` 的 `plugin` 数组中添加缺失的插件:
|
|
149
|
-
- `oh-my-opencode`
|
|
150
|
-
- `opencode-openai-codex-auth`
|
|
151
|
-
|
|
152
|
-
示例 plugin 配置:
|
|
153
|
-
|
|
154
|
-
```json
|
|
155
|
-
"plugin": [
|
|
156
|
-
"oh-my-opencode",
|
|
157
|
-
"opencode-openai-codex-auth"
|
|
158
|
-
]
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
3. 验证安装:
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
grep -E 'oh-my-opencode|opencode-openai-codex-auth' ~/.config/opencode/opencode.json
|
|
165
|
-
```
|
|
166
102
|
|
|
167
103
|
### 3.6 agent-browser 未安装
|
|
168
104
|
|
|
@@ -212,87 +148,15 @@ fi
|
|
|
212
148
|
python --version
|
|
213
149
|
```
|
|
214
150
|
|
|
215
|
-
### 3.
|
|
216
|
-
|
|
217
|
-
**注意:oh-my-opencode 配置应在全局配置文件 `~/.config/opencode/oh-my-opencode.json` 中,而非项目根目录。**
|
|
151
|
+
### 3.9 自动 attach(推荐)
|
|
218
152
|
|
|
219
|
-
|
|
153
|
+
执行 attach(会自动覆盖/新建对应节点,其它不动,并生成备份文件):
|
|
220
154
|
|
|
221
155
|
```bash
|
|
222
|
-
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
2. 使用 Edit 工具添加缺失的配置节点:
|
|
226
|
-
|
|
227
|
-
#### 3.7.1 sisyphus_agent 配置缺失
|
|
228
|
-
|
|
229
|
-
如果根节点缺少 `sisyphus_agent`,使用 Edit 工具添加:
|
|
230
|
-
|
|
231
|
-
```json
|
|
232
|
-
{
|
|
233
|
-
"sisyphus_agent": {
|
|
234
|
-
"disabled": false,
|
|
235
|
-
"default_builder_enabled": true,
|
|
236
|
-
"planner_enabled": true,
|
|
237
|
-
"replace_plan": false
|
|
238
|
-
}
|
|
239
|
-
}
|
|
156
|
+
python3 ~/.opencode/commands/opencode_attach.py
|
|
240
157
|
```
|
|
241
158
|
|
|
242
|
-
注意:这是根节点配置,应添加到 JSON 的第一层级。
|
|
243
159
|
|
|
244
|
-
#### 3.7.2 agents.sisyphus.variant 不是 none
|
|
245
|
-
|
|
246
|
-
如果 `~/.config/opencode/oh-my-opencode.json` 的 `agents.sisyphus.variant` 缺失或不是 `none`,使用 Edit 工具修复为:
|
|
247
|
-
|
|
248
|
-
```json
|
|
249
|
-
{
|
|
250
|
-
"agents": {
|
|
251
|
-
"sisyphus": {
|
|
252
|
-
"variant": "none"
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
3. 验证配置:
|
|
259
|
-
|
|
260
|
-
```bash
|
|
261
|
-
# 检查 sisyphus_agent
|
|
262
|
-
grep -q '"sisyphus_agent"' ~/.config/opencode/oh-my-opencode.json && echo "✅ sisyphus_agent 已配置" || echo "❌ sisyphus_agent 缺失"
|
|
263
|
-
|
|
264
|
-
# 检查 agents.sisyphus.variant
|
|
265
|
-
node -e "const fs=require('node:fs');const os=require('node:os');const p=os.homedir()+'/.config/opencode/oh-my-opencode.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));console.log(j?.agents?.sisyphus?.variant||'MISSING');process.exit(j?.agents?.sisyphus?.variant==='none'?0:1)" && echo "✅ agents.sisyphus.variant=none" || echo "❌ agents.sisyphus.variant 不是 none"
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### 3.8 全局 opencode.json agent 配置缺失
|
|
269
|
-
|
|
270
|
-
**注意:agent 配置应在全局配置文件 `~/.config/opencode/opencode.json` 中,而非项目根目录。**
|
|
271
|
-
|
|
272
|
-
如果 `~/.config/opencode/opencode.json` 缺少 `agent.quick` 或 `agent.middle`,使用 Edit 工具添加:
|
|
273
|
-
|
|
274
|
-
```json
|
|
275
|
-
{
|
|
276
|
-
"agent": {
|
|
277
|
-
"quick": {
|
|
278
|
-
"model": "github-copilot/claude-haiku-4.5"
|
|
279
|
-
},
|
|
280
|
-
"middle": {
|
|
281
|
-
"model": "github-copilot/claude-sonnet-4.5"
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
验证配置:
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
# 检查 agent.quick
|
|
291
|
-
grep -Eq '"agent"[[:space:]]*:' ~/.config/opencode/opencode.json && grep -Eq '"quick"[[:space:]]*:' ~/.config/opencode/opencode.json && echo "✅ agent.quick 已配置" || echo "❌ agent.quick 缺失"
|
|
292
|
-
|
|
293
|
-
# 检查 agent.middle
|
|
294
|
-
grep -Eq '"agent"[[:space:]]*:' ~/.config/opencode/opencode.json && grep -Eq '"middle"[[:space:]]*:' ~/.config/opencode/opencode.json && echo "✅ agent.middle 已配置" || echo "❌ agent.middle 缺失"
|
|
295
|
-
```
|
|
296
160
|
|
|
297
161
|
---
|
|
298
162
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sisyphus_agent": {
|
|
3
|
+
"disabled": false,
|
|
4
|
+
"default_builder_enabled": true,
|
|
5
|
+
"planner_enabled": true,
|
|
6
|
+
"replace_plan": false
|
|
7
|
+
},
|
|
8
|
+
"agents": {
|
|
9
|
+
"sisyphus": {
|
|
10
|
+
"model": "openai/gpt-5.2",
|
|
11
|
+
"variant": "none"
|
|
12
|
+
},
|
|
13
|
+
"oracle": {
|
|
14
|
+
"model": "openai/gpt-5.2",
|
|
15
|
+
"variant": "high"
|
|
16
|
+
},
|
|
17
|
+
"librarian": {
|
|
18
|
+
"model": "github-copilot/gpt-5-mini"
|
|
19
|
+
},
|
|
20
|
+
"explore": {
|
|
21
|
+
"model": "github-copilot/gpt-5-mini"
|
|
22
|
+
},
|
|
23
|
+
"multimodal-looker": {
|
|
24
|
+
"model": "github-copilot/gemini-3-flash-preview"
|
|
25
|
+
},
|
|
26
|
+
"prometheus": {
|
|
27
|
+
"model": "openai/gpt-5.2",
|
|
28
|
+
"variant": "max"
|
|
29
|
+
},
|
|
30
|
+
"metis": {
|
|
31
|
+
"model": "openai/gpt-5.2",
|
|
32
|
+
"variant": "max"
|
|
33
|
+
},
|
|
34
|
+
"momus": {
|
|
35
|
+
"model": "openai/gpt-5.2",
|
|
36
|
+
"variant": "medium"
|
|
37
|
+
},
|
|
38
|
+
"atlas": {
|
|
39
|
+
"model": "openai/gpt-5.2-codex"
|
|
40
|
+
},
|
|
41
|
+
"codex-reviewer": {
|
|
42
|
+
"model": "openai/gpt-5.2-codex",
|
|
43
|
+
"variant": "xhigh",
|
|
44
|
+
"temperature": 0.1
|
|
45
|
+
},
|
|
46
|
+
"gemini-reviewer": {
|
|
47
|
+
"model": "github-copilot/gemini-3-pro-preview",
|
|
48
|
+
"variant": "max"
|
|
49
|
+
},
|
|
50
|
+
"claude-reviewer": {
|
|
51
|
+
"model": "github-copilot/claude-sonnet-4.5",
|
|
52
|
+
"variant": "high"
|
|
53
|
+
},
|
|
54
|
+
"pr-fixer": {
|
|
55
|
+
"model": "openai/gpt-5.2-codex",
|
|
56
|
+
"variant": "xhigh",
|
|
57
|
+
"temperature": 0.1
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"concurrency": 5,
|
|
61
|
+
"categories": {
|
|
62
|
+
"visual-engineering": {
|
|
63
|
+
"model": "github-copilot/gemini-3-pro-preview",
|
|
64
|
+
"variant": "high"
|
|
65
|
+
},
|
|
66
|
+
"ultrabrain": {
|
|
67
|
+
"model": "openai/gpt-5.2-codex",
|
|
68
|
+
"variant": "xhigh"
|
|
69
|
+
},
|
|
70
|
+
"artistry": {
|
|
71
|
+
"model": "github-copilot/gemini-3-pro-preview",
|
|
72
|
+
"variant": "max"
|
|
73
|
+
},
|
|
74
|
+
"quick": {
|
|
75
|
+
"model": "github-copilot/claude-haiku-4.5"
|
|
76
|
+
},
|
|
77
|
+
"middle": {
|
|
78
|
+
"model": "github-copilot/claude-sonnet-4.5"
|
|
79
|
+
},
|
|
80
|
+
"unspecified-low": {
|
|
81
|
+
"model": "github-copilot/claude-sonnet-4.5",
|
|
82
|
+
"variant": "medium"
|
|
83
|
+
},
|
|
84
|
+
"unspecified-high": {
|
|
85
|
+
"model": "openai/gpt-5.2",
|
|
86
|
+
"variant": "medium"
|
|
87
|
+
},
|
|
88
|
+
"writing": {
|
|
89
|
+
"model": "github-copilot/gemini-3-flash-preview"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"instructions": [
|
|
3
|
+
"AGENTS.md",
|
|
4
|
+
"ruler/**/*.md"
|
|
5
|
+
],
|
|
6
|
+
"plugin": [
|
|
7
|
+
"opencode-antigravity-auth",
|
|
8
|
+
"oh-my-opencode",
|
|
9
|
+
"opencode-openai-codex-auth"
|
|
10
|
+
],
|
|
11
|
+
"agent": {
|
|
12
|
+
"quick": {
|
|
13
|
+
"model": "github-copilot/gpt-5-mini"
|
|
14
|
+
},
|
|
15
|
+
"middle": {
|
|
16
|
+
"model": "github-copilot/claude-sonnet-4.5"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"permission": {
|
|
20
|
+
"read": {
|
|
21
|
+
"*.env": "allow",
|
|
22
|
+
"*.env.*": "allow"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"model": "openai/gpt-5.2"
|
|
26
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_plain_object(value):
|
|
13
|
+
return isinstance(value, dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def deep_merge_in_place(target, source):
|
|
17
|
+
if not _is_plain_object(target) or not _is_plain_object(source):
|
|
18
|
+
raise TypeError("deep_merge_in_place expects dicts")
|
|
19
|
+
|
|
20
|
+
for key, s_val in source.items():
|
|
21
|
+
if _is_plain_object(s_val):
|
|
22
|
+
t_val = target.get(key)
|
|
23
|
+
if not _is_plain_object(t_val):
|
|
24
|
+
t_val = {}
|
|
25
|
+
target[key] = t_val
|
|
26
|
+
deep_merge_in_place(t_val, s_val)
|
|
27
|
+
else:
|
|
28
|
+
target[key] = s_val
|
|
29
|
+
|
|
30
|
+
return target
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_json_file(path):
|
|
34
|
+
raw = path.read_text(encoding="utf-8")
|
|
35
|
+
return json.loads(raw)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def atomic_write_json(path, data):
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
tmp_path = path.with_name(f"{path.name}.tmp.{os.getpid()}")
|
|
41
|
+
serialized = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
42
|
+
tmp_path.write_text(serialized, encoding="utf-8")
|
|
43
|
+
os.replace(tmp_path, path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def backup_file(path):
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
ts = time.strftime("%Y%m%d%H%M%S")
|
|
50
|
+
bak = path.with_name(f"{path.name}.bak.{ts}")
|
|
51
|
+
shutil.copy2(path, bak)
|
|
52
|
+
return bak
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def attach(source_path, target_path, *, make_backup=True, dry_run=False):
|
|
56
|
+
source = load_json_file(source_path)
|
|
57
|
+
if not _is_plain_object(source):
|
|
58
|
+
raise ValueError(f"Source JSON root must be an object: {source_path}")
|
|
59
|
+
|
|
60
|
+
if target_path.exists():
|
|
61
|
+
target = load_json_file(target_path)
|
|
62
|
+
if not _is_plain_object(target):
|
|
63
|
+
raise ValueError(f"Target JSON root must be an object: {target_path}")
|
|
64
|
+
else:
|
|
65
|
+
target = {}
|
|
66
|
+
|
|
67
|
+
merged = deep_merge_in_place(target, source)
|
|
68
|
+
|
|
69
|
+
bak = None
|
|
70
|
+
if make_backup and target_path.exists() and not dry_run:
|
|
71
|
+
bak = backup_file(target_path)
|
|
72
|
+
|
|
73
|
+
if not dry_run:
|
|
74
|
+
atomic_write_json(target_path, merged)
|
|
75
|
+
|
|
76
|
+
return bak
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(argv):
|
|
80
|
+
parser = argparse.ArgumentParser(
|
|
81
|
+
description=(
|
|
82
|
+
"Attach JSON fragments into OpenCode global config files. "
|
|
83
|
+
"Rule: deep-merge objects; replace arrays/primitives; preserve other keys."
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--oh-source",
|
|
88
|
+
default=str(Path(__file__).resolve().parents[1] / "commands" / "oh_attach.json"),
|
|
89
|
+
help="Path to oh_attach.json",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--opencode-source",
|
|
93
|
+
default=str(Path(__file__).resolve().parents[1] / "commands" / "opencode_attach.json"),
|
|
94
|
+
help="Path to opencode_attach.json",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--config-dir",
|
|
98
|
+
default=str(Path.home() / ".config" / "opencode"),
|
|
99
|
+
help="Config directory (default: ~/.config/opencode)",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--no-backup", action="store_true", help="Do not create .bak files")
|
|
102
|
+
parser.add_argument("--dry-run", action="store_true", help="Do not write files")
|
|
103
|
+
|
|
104
|
+
args = parser.parse_args(argv)
|
|
105
|
+
|
|
106
|
+
config_dir = Path(os.path.expanduser(args.config_dir)).resolve()
|
|
107
|
+
oh_target = config_dir / "oh-my-opencode.json"
|
|
108
|
+
opencode_target = config_dir / "opencode.json"
|
|
109
|
+
|
|
110
|
+
oh_source = Path(args.oh_source).resolve()
|
|
111
|
+
opencode_source = Path(args.opencode_source).resolve()
|
|
112
|
+
|
|
113
|
+
if not oh_source.exists():
|
|
114
|
+
raise FileNotFoundError(f"Missing source file: {oh_source}")
|
|
115
|
+
if not opencode_source.exists():
|
|
116
|
+
raise FileNotFoundError(f"Missing source file: {opencode_source}")
|
|
117
|
+
|
|
118
|
+
make_backup = not args.no_backup
|
|
119
|
+
dry_run = args.dry_run
|
|
120
|
+
|
|
121
|
+
bak1 = attach(oh_source, oh_target, make_backup=make_backup, dry_run=dry_run)
|
|
122
|
+
bak2 = attach(opencode_source, opencode_target, make_backup=make_backup, dry_run=dry_run)
|
|
123
|
+
|
|
124
|
+
if dry_run:
|
|
125
|
+
print("DRY_RUN: no files written")
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
if bak1:
|
|
129
|
+
print(f"backup: {bak1}")
|
|
130
|
+
if bak2:
|
|
131
|
+
print(f"backup: {bak2}")
|
|
132
|
+
print(f"updated: {oh_target}")
|
|
133
|
+
print(f"updated: {opencode_target}")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
try:
|
|
139
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
142
|
+
raise SystemExit(1)
|
|
@@ -58,24 +58,32 @@ agent: sisyphus
|
|
|
58
58
|
- 取出:`contextFile`、`runId`、`headOid`(如有)
|
|
59
59
|
- **CRITICAL**: 必须等待此 Task 成功完成并获取到 `contextFile` 后,才能进入 Step 2
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
**检查 Decision Log**:
|
|
62
|
+
- 检查是否存在 `./.cache/decision-log-pr{{PR_NUMBER}}.md`
|
|
63
|
+
- 如存在,将路径记录为 `decisionLogFile`(用于后续步骤)
|
|
64
|
+
- 如不存在,`decisionLogFile` 为空或不传递
|
|
62
65
|
|
|
63
|
-
-
|
|
66
|
+
2. Task(并行): `codex-reviewer` + `claude-reviewer` + `gemini-reviewer` + `gh-thread-reviewer` **(依赖 Step 1 的 contextFile 和 decisionLogFile)**
|
|
67
|
+
|
|
68
|
+
- **DEPENDENCY**: 这些 reviewers 依赖 Step 1 返回的 `contextFile` 和 `decisionLogFile`(如存在),因此**必须等 Step 1 完成后才能并行启动**
|
|
64
69
|
- 每个 reviewer prompt 必须包含:
|
|
65
70
|
- `PR #{{PR_NUMBER}}`
|
|
66
71
|
- `round: <ROUND>`
|
|
67
72
|
- `runId: <RUN_ID>`(来自 Step 1 的输出,必须透传,禁止自行生成)
|
|
68
73
|
- `contextFile: ./.cache/<file>.md`(来自 Step 1 的输出)
|
|
69
|
-
-
|
|
74
|
+
- `decisionLogFile: ./.cache/decision-log-pr{{PR_NUMBER}}.md`(如存在,来自检查后得出)
|
|
75
|
+
- reviewer 默认读 `contextFile`;如果 `decisionLogFile` 存在,reviewer 应在 prompt 中提供该文件路径以参考前轮决策;必要时允许用 `git/gh` 只读命令拿 diff
|
|
70
76
|
- 忽略问题:1.格式化代码引起的噪音 2.已经lint检查以外的格式问题 3.忽略单元测试不足的问题
|
|
71
77
|
- 特别关注: 逻辑、安全、性能、可维护性
|
|
72
78
|
- 同时要注意 pr 前面轮次的 修复和讨论,对于已经拒绝、已修复的问题不要反复的提出
|
|
73
79
|
- 同时也要注意fix的过程中有没有引入新的问题。
|
|
80
|
+
|
|
81
|
+
备注:fixFile 分为 `IssuesToFix`(P0/P1,必须修)与 `OptionalIssues`(P2/P3,pr-fix 自主裁决)。
|
|
74
82
|
- 每个 reviewer 输出:`reviewFile: ./.cache/<file>.md`(Markdown)
|
|
75
83
|
|
|
76
84
|
3. Task: `pr-review-aggregate`
|
|
77
85
|
|
|
78
|
-
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`contextFile: ./.cache/<file>.md`、以及 1+ 条 `reviewFile: ./.cache/<file>.md`
|
|
86
|
+
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`contextFile: ./.cache/<file>.md`、以及 1+ 条 `reviewFile: ./.cache/<file>.md`、以及 `decisionLogFile: ./.cache/decision-log-pr{{PR_NUMBER}}.md`(如存在)
|
|
79
87
|
- 输出:`{"stop":true}` 或 `{"stop":false,"fixFile":"..."}`
|
|
80
88
|
- 若 `stop=true`:本轮结束并退出循环
|
|
81
89
|
- **唯一性约束**: 每轮只能发布一次 Review Summary;脚本内置幂等检查,重复调用不会重复发布
|
|
@@ -97,6 +105,10 @@ agent: sisyphus
|
|
|
97
105
|
- 输出:`{"ok":true}`
|
|
98
106
|
- **唯一性约束**: 每轮只能发布一次 Fix Report;脚本内置幂等检查,重复调用不会重复发布
|
|
99
107
|
|
|
108
|
+
**Decision Log 更新**:
|
|
109
|
+
- `pr-fix` agent 在修复过程中会在 `./.cache/decision-log-pr{{PR_NUMBER}}.md` 中追加本轮决策(Fixed/Rejected)
|
|
110
|
+
- 下一轮 review 将自动使用更新后的 decision-log,避免重复提出已决策问题
|
|
111
|
+
|
|
100
112
|
6. 下一轮
|
|
101
113
|
|
|
102
114
|
- 回到 1(进入下一轮 reviewers)
|
package/README.md
CHANGED
|
@@ -128,6 +128,63 @@ target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.jso
|
|
|
128
128
|
|
|
129
129
|
查看 `example/`:包含一个最小可读的 `dx/config` 配置示例,以及如何在一个 pnpm+nx monorepo 中接入 dx。
|
|
130
130
|
|
|
131
|
+
## PR Review Loop(自动评审-修复闭环)
|
|
132
|
+
|
|
133
|
+
dx 内置一套 PR 评审自动化工作流:并行评审 → 聚合结论 → 生成修复清单 → 自动修复 → 再评审,最多循环 3 轮,用于让 PR 更快收敛。
|
|
134
|
+
|
|
135
|
+
### 什么时候用
|
|
136
|
+
|
|
137
|
+
- PR 变更较大、想要更系统地覆盖安全/性能/可维护性问题
|
|
138
|
+
- 希望在 CI 通过的前提下,把评审建议落成可执行的修复清单(fixFile)
|
|
139
|
+
- 希望避免同一个问题在不同轮次被反复提出(通过 Decision Log 机制)
|
|
140
|
+
|
|
141
|
+
### 如何运行
|
|
142
|
+
|
|
143
|
+
在创建 PR 后执行:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
/pr-review-loop --pr <PR_NUMBER>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
更多命令说明见:`@opencode/commands/pr-review-loop.md`。
|
|
150
|
+
|
|
151
|
+
提示:在创建 PR 的流程中也会给出快捷入口,见:`@opencode/commands/git-commit-and-pr.md`。
|
|
152
|
+
|
|
153
|
+
### 工作流概览
|
|
154
|
+
|
|
155
|
+
- 预检(`pr-precheck`):先做编译/预检 gate,不通过则进入修复再预检(最多 2 次)
|
|
156
|
+
- 获取上下文(`pr-context`):生成本轮上下文缓存 `contextFile`
|
|
157
|
+
- 并行评审(4 个 reviewer):`codex-reviewer` / `claude-reviewer` / `gemini-reviewer` / `gh-thread-reviewer`
|
|
158
|
+
- 聚合(`pr-review-aggregate`):合并各 reviewer 结果、去重、输出 `fixFile`,并发布本轮 Review Summary
|
|
159
|
+
- 修复(`pr-fix`):按 `fixFile` 逐条修复(每条 findingId 单独 commit + push),输出 `fixReportFile`
|
|
160
|
+
- 发布修复报告(`pr-review-aggregate` 模式 B):发布 Fix Report
|
|
161
|
+
|
|
162
|
+
备注:每轮发布到 PR 的评论都会带 `<!-- pr-review-loop-marker -->`,用于幂等与避免反复采集机器人评论。
|
|
163
|
+
|
|
164
|
+
### 缓存文件(项目内 ./\.cache/)
|
|
165
|
+
|
|
166
|
+
该流程的中间产物都写入项目内 `./.cache/`,并在 agent/命令之间传递 repo 相对路径:
|
|
167
|
+
|
|
168
|
+
- `./.cache/pr-context-pr<PR>-r<ROUND>-<RUN_ID>.md`(contextFile)
|
|
169
|
+
- `./.cache/review-<REVIEWER>-pr<PR>-r<ROUND>-<RUN_ID>.md`(reviewFile)
|
|
170
|
+
- `./.cache/fix-pr<PR>-r<ROUND>-<RUN_ID>.md`(fixFile)
|
|
171
|
+
- `./.cache/fix-report-pr<PR>-r<ROUND>-<RUN_ID>.md`(fixReportFile)
|
|
172
|
+
|
|
173
|
+
### Decision Log(跨轮次决策日志,用于收敛)
|
|
174
|
+
|
|
175
|
+
为了解决“第一轮拒绝的问题在后续轮次反复出现”的问题,PR Review Loop 使用 Decision Log 持久化每轮的决策:
|
|
176
|
+
|
|
177
|
+
- 文件:`./.cache/decision-log-pr<PR_NUMBER>.md`
|
|
178
|
+
- 生成者:`pr-fix` 在修复完成后创建/追加(append-only,禁止覆盖历史)
|
|
179
|
+
- 内容:记录每轮的 Fixed/Rejected,以及 `essence`(问题本质的一句话描述,用于后续智能匹配)
|
|
180
|
+
|
|
181
|
+
在后续轮次:
|
|
182
|
+
|
|
183
|
+
- reviewers 若收到 `decisionLogFile`,必须读取并遵守:已修复不再提、已拒绝不再提(除非严重性升级)
|
|
184
|
+
- aggregate 在模式 A 中基于 LLM 对比 `essence` 做“问题本质相同”的判断,并生成 `escalation_groups` 入参给脚本
|
|
185
|
+
|
|
186
|
+
升级质疑规则:只有当新 finding 的优先级比历史 rejected 高 ≥2 级(例如 P3→P1、P2→P0)时,才允许重新打开。
|
|
187
|
+
|
|
131
188
|
## 命令
|
|
132
189
|
|
|
133
190
|
dx 的命令由 `dx/config/commands.json` 驱动,并且内置了一些 internal runner(避免项目侧依赖任何 `scripts/lib/*.js`):
|
package/lib/opencode-initial.js
CHANGED
|
@@ -27,8 +27,8 @@ async function collectTemplateFiles(dir) {
|
|
|
27
27
|
}
|
|
28
28
|
if (!entry.isFile()) continue
|
|
29
29
|
const lowerName = entry.name.toLowerCase()
|
|
30
|
-
// 拷贝 .md
|
|
31
|
-
if (!lowerName.endsWith('.md') && !lowerName.endsWith('.py')) continue
|
|
30
|
+
// 拷贝 .md / .py / .json 文件
|
|
31
|
+
if (!lowerName.endsWith('.md') && !lowerName.endsWith('.py') && !lowerName.endsWith('.json')) continue
|
|
32
32
|
// 跳过 Python 编译文件
|
|
33
33
|
if (lowerName.endsWith('.pyc') || lowerName.endsWith('.pyo') || lowerName.endsWith('.pyd')) continue
|
|
34
34
|
out.push(full)
|
|
@@ -59,6 +59,7 @@ async function copyTemplateTree({ srcDir, dstDir }) {
|
|
|
59
59
|
const files = await collectTemplateFiles(srcDir)
|
|
60
60
|
let mdCount = 0
|
|
61
61
|
let pyCount = 0
|
|
62
|
+
let jsonCount = 0
|
|
62
63
|
|
|
63
64
|
for (const file of files) {
|
|
64
65
|
const rel = relative(srcDir, file)
|
|
@@ -75,10 +76,12 @@ async function copyTemplateTree({ srcDir, dstDir }) {
|
|
|
75
76
|
} catch {
|
|
76
77
|
// 忽略权限设置失败
|
|
77
78
|
}
|
|
79
|
+
} else if (file.endsWith('.json')) {
|
|
80
|
+
jsonCount++
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
return { total: files.length, md: mdCount, py: pyCount }
|
|
84
|
+
return { total: files.length, md: mdCount, py: pyCount, json: jsonCount }
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
function resolveTemplateRoot(packageRoot) {
|
|
@@ -125,6 +128,14 @@ export async function runOpenCodeInitial(options = {}) {
|
|
|
125
128
|
const commandsStats = await copyTemplateTree({ srcDir: srcCommands, dstDir: dstCommands })
|
|
126
129
|
|
|
127
130
|
logger.success(`已初始化 OpenCode 模板到: ${dstRoot}`)
|
|
128
|
-
logger.info(
|
|
129
|
-
|
|
131
|
+
logger.info(
|
|
132
|
+
`agents: ${agentsStats.md} 个 .md 文件` +
|
|
133
|
+
`${agentsStats.py > 0 ? ` + ${agentsStats.py} 个 .py 文件` : ''}` +
|
|
134
|
+
`${agentsStats.json > 0 ? ` + ${agentsStats.json} 个 .json 文件` : ''}`
|
|
135
|
+
)
|
|
136
|
+
logger.info(
|
|
137
|
+
`commands: ${commandsStats.md} 个 .md 文件` +
|
|
138
|
+
`${commandsStats.py > 0 ? ` + ${commandsStats.py} 个 .py 文件` : ''}` +
|
|
139
|
+
`${commandsStats.json > 0 ? ` + ${commandsStats.json} 个 .json 文件` : ''}`
|
|
140
|
+
)
|
|
130
141
|
}
|