@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.
@@ -18,7 +18,7 @@ pnpm add -g @ranger1/dx@latest && dx initial
18
18
 
19
19
  ## Step 1: 并行检测
20
20
 
21
- **同时执行以下 3 个 Bash 调用(真正并行):**
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' ~/.config/opencode/opencode.json 2>/dev/null && echo "INSTALLED" || echo "NOT_INSTALLED");
43
- echo "opencode-openai-codex-auth:" && (grep -q 'opencode-openai-codex-auth' ~/.config/opencode/opencode.json 2>/dev/null && echo "INSTALLED" || echo "NOT_INSTALLED");
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: oh-my-opencode.json 配置检测
48
- echo "=== OMO_CONFIG ===";
49
- echo "sisyphus_agent:" && (grep -q '"sisyphus_agent"' ~/.config/opencode/oh-my-opencode.json 2>/dev/null && echo "CONFIGURED" || echo "NOT_CONFIGURED");
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
- 全局 sisyphus_agent 配置 | <状态> | -
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.7 全局 oh-my-opencode.json 配置缺失
216
-
217
- **注意:oh-my-opencode 配置应在全局配置文件 `~/.config/opencode/oh-my-opencode.json` 中,而非项目根目录。**
151
+ ### 3.9 自动 attach(推荐)
218
152
 
219
- 1. 先读取现有全局配置:
153
+ 执行 attach(会自动覆盖/新建对应节点,其它不动,并生成备份文件):
220
154
 
221
155
  ```bash
222
- cat ~/.config/opencode/oh-my-opencode.json
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
- 2. Task(并行): `codex-reviewer` + `claude-reviewer` + `gemini-reviewer` + `gh-thread-reviewer` **(依赖 Step 1 的 contextFile)**
61
+ **检查 Decision Log**:
62
+ - 检查是否存在 `./.cache/decision-log-pr{{PR_NUMBER}}.md`
63
+ - 如存在,将路径记录为 `decisionLogFile`(用于后续步骤)
64
+ - 如不存在,`decisionLogFile` 为空或不传递
62
65
 
63
- - **DEPENDENCY**: 这些 reviewers 依赖 Step 1 返回的 `contextFile`,因此**必须等 Step 1 完成后才能并行启动**
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
- - reviewer 默认读 `contextFile`;必要时允许用 `git/gh` 只读命令拿 diff
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`):
@@ -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 .py 文件
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(`agents: ${agentsStats.md} 个 .md 文件${agentsStats.py > 0 ? ` + ${agentsStats.py} 个 .py 文件` : ''}`)
129
- logger.info(`commands: ${commandsStats.md} 个 .md 文件${commandsStats.py > 0 ? ` + ${commandsStats.py} 个 .py 文件` : ''}`)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {