dsh-issue2pr 0.1.0

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.
@@ -0,0 +1,313 @@
1
+ // lib/stageConfig.js — 阶段级配置模型(能力表 / 默认提示词 / 合并 / 委托任务包)
2
+ // 配置存于 project.json 的 stageConfig 字段:{ P1: { prompts, provider, model,
3
+ // reasoningEffort, timeoutMs, maxTokens, delegate }, … };未配置项回落默认值。
4
+ // STAGE_DEFS 的 prompts 与各阶段文件中的硬编码提示词保持 1:1(改默认值两处同步改)。
5
+ import { existsSync, readdirSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { readArtifact } from "./store.js";
8
+ import { readTriggerText } from "./stages/helpers.js";
9
+
10
+ const CODER_SYSTEM = [
11
+ "你是 Coder Sub-Agent,纪律(superpowers):",
12
+ "1. TDD:涉及行为修改时先产出失败测试,再产出实现;",
13
+ "2. 只输出 unified diff(--- a/ +++ b/),最小修改范围,永不整文件覆盖;",
14
+ "3. 外科手术式改动:不顺手改进相邻代码;",
15
+ "4. verify-before-claim:不得声称未验证的结论。",
16
+ ].join("\n");
17
+
18
+ const P3_SYSTEM = [
19
+ "你是 Code Understanding。基于候选文件的真实源码(每行前缀「行号|」为该文件真实行号)做深度调用链分析,",
20
+ "输出中文 markdown,三个二级小节标题固定为「## 关键函数」「## 调用方」「## 潜在修改点」。要求:",
21
+ "「## 关键函数」按文件分「### 相对路径」子节,每个函数一条:定位锚点(格式 `路径:行号`)+ 一句话职责",
22
+ "+ 关键细节(参数含义、分支条件、常量取值、返回值、副作用),片段内看不到的明说「片段内不可见」;",
23
+ "「## 调用方」按入口/主题分子节,每条链路完整写 A() → B() → C() 并逐步说明数据与控制流,",
24
+ "候选文件之外的调用方标注「片段外,推测」;",
25
+ "「## 潜在修改点」按与 Issue 目标的关联度降序排列,每条给:定位(文件+函数+`路径:行号` 锚点)/",
26
+ "为什么关联 Issue/建议改法/影响面与风险。",
27
+ "纪律:只依据提供的源码,不臆造行号、函数或调用关系;输入被截断的部分如实说明;宁详勿略,但重复与空话不写。",
28
+ ].join("");
29
+
30
+ // 每阶段定义:name/desc(UI);prompts(默认提示词,key ""=单角色);
31
+ // caps(可配能力:route=模型/思考深度,exec=超时/maxTokens,delegate=委托外部,test=测试命令);
32
+ // params(阶段专属工具参数元数据:def 默认值 / type string|number / label+unit 供 UI 渲染;
33
+ // 开源原则:凡影响执行行为的数值与路径不硬编码,一律在此声明并可被 project.json 覆盖);
34
+ // delegateSpec(委托开放阶段的任务包构成:inputs 上游产物 / output 输出产物 / contract 输出契约)
35
+ export const STAGE_DEFS = {
36
+ P1: {
37
+ name: "IssueAnalyzer", desc: "Issue → 结构化契约",
38
+ prompts: { "": "你是 IssueAnalyzer。把自然语言 Issue 提炼为结构化契约,只输出 JSON,不要输出其他文字。" },
39
+ caps: { route: true, exec: true, delegate: true },
40
+ delegateSpec: { inputs: "trigger", output: "01-issue-analysis.json",
41
+ contract: '{"phenomenon":"现象","trigger":"触发条件","scope":["影响模块"],"success_criteria":["可验证成功标准"],"constraints":["约束"],"risk_level":"low|medium|high"}' },
42
+ },
43
+ P2: {
44
+ name: "Search Layer", desc: "候选文件 + 证据",
45
+ prompts: { "": "你是 Search Layer。根据 Issue 契约和仓库文件清单选出候选文件,每条必须给 SearchEvidence。只输出 JSON。" },
46
+ caps: { route: true, exec: true, delegate: true },
47
+ params: {
48
+ repoScanMax: { def: 400, type: "number", label: "仓库清单扫描上限", unit: "个文件",
49
+ hint: "P2 把文件清单喂给 LLM 选候选;超大仓库截断防 prompt 膨胀" },
50
+ },
51
+ delegateSpec: { inputs: ["01-issue-analysis.json", "repo"], output: "02-search-candidates.json",
52
+ contract: '{"candidates":[{"path":"相对路径","role":"模块角色","evidence":"选择理由","confidence":"high|medium|low"}],"test_candidates":["测试文件"],"uncertain":["需进一步探索项"]}' },
53
+ },
54
+ P3: {
55
+ name: "Code Understanding", desc: "调用链与修改点",
56
+ prompts: { "": P3_SYSTEM },
57
+ caps: { route: true, exec: true, delegate: true },
58
+ params: {
59
+ deepReadFiles: { def: 6, type: "number", label: "深读候选文件数", unit: "个",
60
+ hint: "按 P2 候选置信度顺序取前 N 个读全文" },
61
+ fileChars: { def: 6000, type: "number", label: "单文件读取上限", unit: "字",
62
+ hint: "超长文件截断,防止上下文溢出" },
63
+ },
64
+ delegateSpec: { inputs: ["02-search-candidates.json", "repo"], output: "03-code-understanding.md",
65
+ contract: "中文 markdown,含「## 关键函数」「## 调用方」「## 潜在修改点」三节;关键函数带 `路径:行号` 锚点与关键细节,调用方给完整链路(片段外标注推测),修改点按与 Issue 关联度排序并说明影响面" },
66
+ },
67
+ P4: {
68
+ name: "Hypothesis", desc: "可验证根因假设",
69
+ prompts: { "": "你是诊断模块。每个根因假设必须携带证据、验证文件与验证方法,禁止「我看着像」式结论。只输出 JSON。" },
70
+ caps: { route: true, exec: true, delegate: true },
71
+ delegateSpec: { inputs: ["03-code-understanding.md"], output: "04-hypotheses.json",
72
+ contract: '{"hypotheses":[{"id":"A","title":"假设","evidence":"来自报告的证据","verify_file":"验证文件","verify_method":"可执行的验证方法"}]}' },
73
+ },
74
+ P5: {
75
+ name: "Planner", desc: "TaskGraph 规划 · 复核门",
76
+ prompts: { "": "你是 Planner。把修复任务拆成有依赖关系的 TaskGraph,每节点可独立验证。只输出 JSON。" },
77
+ caps: { route: true, exec: true, delegate: true },
78
+ delegateSpec: { inputs: ["04-hypotheses.json"], output: "05-task-graph.json",
79
+ contract: '{"nodes":[{"id":"T1","title":"任务","input":"前置 artifact","output":"本任务 artifact","deps":["T0"],"success_criteria":"可验证通过条件","risk":"low|medium|high"}],"review_gate":"节点id","pr_gate":"节点id"}' },
80
+ },
81
+ P6: {
82
+ name: "代码优化", desc: "多智能体协同 · 复核门",
83
+ prompts: {
84
+ planner: "你是多智能体 Planner。把 TaskGraph 节点派给 Coder,每单指定目标文件。只输出 JSON。",
85
+ coder: CODER_SYSTEM,
86
+ reviewer: "你是 Reviewer Sub-Agent。审 diff:范围是否最小、是否越权、是否遗漏调用方。只输出 JSON。",
87
+ },
88
+ caps: { route: true, exec: true, delegate: true },
89
+ params: {
90
+ claudeBin: { def: "", type: "string", label: "claude 可执行文件",
91
+ hint: "委托 Claude Code 模式用;留空 = 自动探测(PATH → 常见安装位置)。开源环境路径各异,建议显式填写,如 C:\\Users\\you\\AppData\\Roaming\\npm\\claude.cmd" },
92
+ claudeTimeoutMin: { def: 120, type: "number", label: "claude 执行超时", unit: "分钟",
93
+ hint: "无人值守执行任务包的最长等待;超时回退等人工会话" },
94
+ },
95
+ delegateSpec: { inputs: ["05-task-graph.json"], output: "06-implementation/",
96
+ contract: "patches/*.diff(unified diff)逐任务一份 + coder-report.json(含 patches 清单与结论)" },
97
+ },
98
+ P7: {
99
+ name: "Patch Pipeline", desc: "版本校验 → 落盘 + ledger",
100
+ prompts: {}, caps: {},
101
+ },
102
+ P8: {
103
+ name: "TestRunner", desc: "沙箱真实执行",
104
+ prompts: {}, caps: { exec: true, test: true },
105
+ },
106
+ P9: {
107
+ name: "Reviewer", desc: "三维门控审查 · 复核门",
108
+ prompts: { "": "你是 Reviewer Agent。三维门控:①Diff 范围(过大/越权/遗漏调用方)②API 与安全 ③测试补强与说明忠实。测试通过 ≠ 可合并。只输出 JSON。" },
109
+ caps: { route: true, exec: true, delegate: true },
110
+ params: {
111
+ diffChars: { def: 1200, type: "number", label: "diff 每文件载入上限", unit: "字",
112
+ hint: "审查时每份 diff 只载入头部 N 字(路径 + hunk 概览足够范围裁决);调大更全面但易撑爆 prompt" },
113
+ },
114
+ delegateSpec: { inputs: ["06-implementation/coder-report.json", "07-test-report.json"], output: "08-review-report.json",
115
+ contract: '{"diff_scope":"结论","api_security":"结论","tests":"结论","verdict":"pass|fail"}' },
116
+ },
117
+ P10: {
118
+ name: "FailureClassifier", desc: "失败旁路 · 仅失败时执行",
119
+ prompts: { "": "你是 FailureClassifier。把失败归入六类之一并给出处理路径。只输出 JSON。" },
120
+ caps: { route: true, exec: true, delegate: true },
121
+ delegateSpec: { inputs: "failure", output: "09-failure-analysis.json",
122
+ contract: '{"category":"类别(实现错误/根因错误/测试选择/环境缺失/权限被拒/反复失败)","detail":"依据","action":"replan|rollback|escalate"}' },
123
+ },
124
+ P11: {
125
+ name: "PRBuilder + Eval", desc: "PR 说明 + Gate 评测 · 复核门",
126
+ prompts: {
127
+ desc: "你是 PRBuilder。生成忠实反映修改与验证过程的 PR 说明(中文 markdown):背景/根因/修改点/验证证据/风险。禁止夸大。",
128
+ gate: "你是 EvaluationRunner。按 Gate 六项判定:ROOT 根因有证据 / PATCH 干净应用 / TEST 无回归 / DIFF 可审查 / DESC 说明忠实 / ACCEPT 门控通过。只输出 JSON。",
129
+ },
130
+ caps: { route: true, exec: true, delegate: true },
131
+ delegateSpec: { inputs: ["01-issue-analysis.json", "08-review-report.json", "07-test-report.json"], output: "10-pr-description.md",
132
+ contract: "10-pr-description.md(中文 markdown PR 说明)+ 11-eval-report.json({\"ROOT\":\"pass|fail\",…六项})" },
133
+ },
134
+ };
135
+
136
+ export const DEFAULT_LLM_TIMEOUT_MS = 300000; // 5 分钟(与历史行为一致)
137
+ export const DEFAULT_TEST_TIMEOUT_MS = 300000;
138
+
139
+ // —— 合并:用户配置覆盖默认;任何缺省回落 ——
140
+ // params 合并规则:按 STAGE_DEFS 元数据逐键取(用户值 ?? def),未知键忽略
141
+ export function stageCfgOf(project, stageId) {
142
+ const def = STAGE_DEFS[stageId] || {};
143
+ const user = (project && project.stageConfig && project.stageConfig[stageId]) || {};
144
+ const prompts = {};
145
+ for (const key of Object.keys(def.prompts || {})) prompts[key] = (user.prompts && user.prompts[key]) || def.prompts[key];
146
+ const params = {};
147
+ for (const [k, meta] of Object.entries(def.params || {})) {
148
+ const uv = user.params && user.params[k];
149
+ params[k] = meta.type === "string"
150
+ ? (uv != null && uv !== "" ? String(uv) : meta.def)
151
+ : (Number.isFinite(Number(uv)) && Number(uv) > 0 ? Number(uv) : meta.def);
152
+ }
153
+ return {
154
+ prompts,
155
+ params,
156
+ provider: user.provider || "",
157
+ model: user.model || "",
158
+ reasoningEffort: user.reasoningEffort || "",
159
+ timeoutMs: Number(user.timeoutMs) > 0 ? Number(user.timeoutMs) : 0,
160
+ maxTokens: Number(user.maxTokens) > 0 ? Number(user.maxTokens) : 0,
161
+ delegate: {
162
+ mode: (user.delegate && user.delegate.mode) || "off",
163
+ agent: (user.delegate && user.delegate.agent) || "",
164
+ brief: (user.delegate && user.delegate.brief) || "",
165
+ },
166
+ };
167
+ }
168
+
169
+ // 阶段执行器取提示词:rcx.stageCfgOf(id).prompts[key](stageCfgOf 由 buildRcx 注入;缺省时回落 STAGE_DEFS 默认)
170
+ export function sysOf(rcx, stageId, key) {
171
+ const cfg = typeof rcx.stageCfgOf === "function" ? rcx.stageCfgOf(stageId) : null;
172
+ return (cfg && cfg.prompts && cfg.prompts[key || ""]) || STAGE_DEFS[stageId].prompts[key || ""];
173
+ }
174
+
175
+ // 阶段执行器取专属工具参数(裸 rcx 单测无 stageCfgOf 时回落 STAGE_DEFS 默认值)
176
+ export function paramsOf(rcx, stageId) {
177
+ const cfg = typeof rcx.stageCfgOf === "function" ? rcx.stageCfgOf(stageId) : null;
178
+ if (cfg && cfg.params) return cfg.params;
179
+ const out = {};
180
+ for (const [k, m] of Object.entries((STAGE_DEFS[stageId] || {}).params || {})) out[k] = m.def;
181
+ return out;
182
+ }
183
+
184
+ // LLM 路由/执行覆盖(makeLlm 的 overridesOf 用)
185
+ export function routeOverridesOf(rcx) {
186
+ const cfg = rcx.stageCfgOf();
187
+ return {
188
+ provider: cfg.provider || "",
189
+ model: cfg.model || "",
190
+ reasoningEffort: cfg.reasoningEffort || "",
191
+ timeoutMs: cfg.timeoutMs || 0,
192
+ maxTokens: cfg.maxTokens || 0,
193
+ };
194
+ }
195
+
196
+ // —— 校验(validateProject 调用;宽松策略:只查形态,不限制自由文本) ——
197
+ export function validateStageConfig(sc) {
198
+ if (sc === undefined || sc === null) return [true, "ok"];
199
+ if (typeof sc !== "object" || Array.isArray(sc)) return [false, "stageConfig 必须是对象"];
200
+ for (const [id, cfg] of Object.entries(sc)) {
201
+ if (!STAGE_DEFS[id]) return [false, "stageConfig 含未知阶段: " + id];
202
+ if (cfg === null || typeof cfg !== "object") return [false, "stageConfig." + id + " 必须是对象"];
203
+ for (const field of ["provider", "model", "reasoningEffort"]) {
204
+ const v = cfg[field];
205
+ if (v !== undefined && v !== null && typeof v !== "string") return [false, `stageConfig.${id}.${field} 必须是字符串`];
206
+ }
207
+ for (const field of ["timeoutMs", "maxTokens"]) {
208
+ const v = cfg[field];
209
+ if (v !== undefined && v !== null && (typeof v !== "number" || !Number.isFinite(v) || v < 0)) {
210
+ return [false, `stageConfig.${id}.${field} 必须是非负数字`];
211
+ }
212
+ }
213
+ if (cfg.prompts !== undefined) {
214
+ if (typeof cfg.prompts !== "object" || cfg.prompts === null || Array.isArray(cfg.prompts)) {
215
+ return [false, `stageConfig.${id}.prompts 必须是对象`];
216
+ }
217
+ for (const v of Object.values(cfg.prompts)) {
218
+ if (typeof v !== "string") return [false, `stageConfig.${id}.prompts 的值必须是字符串`];
219
+ }
220
+ }
221
+ if (cfg.params !== undefined && cfg.params !== null) {
222
+ if (typeof cfg.params !== "object" || Array.isArray(cfg.params)) {
223
+ return [false, `stageConfig.${id}.params 必须是对象`];
224
+ }
225
+ const defParams = (STAGE_DEFS[id] && STAGE_DEFS[id].params) || {};
226
+ for (const [k, v] of Object.entries(cfg.params)) {
227
+ const meta = defParams[k];
228
+ if (!meta) return [false, `stageConfig.${id}.params 含未知参数: ${k}`];
229
+ if (meta.type === "string") {
230
+ if (typeof v !== "string") return [false, `stageConfig.${id}.params.${k} 必须是字符串`];
231
+ } else if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
232
+ return [false, `stageConfig.${id}.params.${k} 必须是正数`];
233
+ }
234
+ }
235
+ }
236
+ if (cfg.delegate !== undefined && cfg.delegate !== null) {
237
+ const d = cfg.delegate;
238
+ if (typeof d !== "object") return [false, `stageConfig.${id}.delegate 必须是对象`];
239
+ if (d.mode !== undefined && !["off", "session"].includes(d.mode)) {
240
+ return [false, `stageConfig.${id}.delegate.mode 仅允许 off|session`];
241
+ }
242
+ if (d.agent !== undefined && typeof d.agent !== "string") return [false, `stageConfig.${id}.delegate.agent 必须是字符串`];
243
+ if (d.brief !== undefined && typeof d.brief !== "string") return [false, `stageConfig.${id}.delegate.brief 必须是字符串`];
244
+ }
245
+ }
246
+ return [true, "ok"];
247
+ }
248
+
249
+ // —— 委托外部智能体:阶段不再本机调 LLM,而是产出任务包交外部智能体执行,
250
+ // 复核门按产出就绪判定放行 ——
251
+ // P6 与项目页「P6 执行模式」联动:p6Mode=session(人工外部会话)/ claude(claude CLI
252
+ // 自动执行)/ 委托开关任一开启,均为委托态
253
+ export function stageDelegated(rcx, stageId) {
254
+ const def = STAGE_DEFS[stageId];
255
+ if (!def || !def.caps.delegate) return false;
256
+ // p6Mode 在 rcx 顶层(buildRcx)或 run 上(裸 rcx 单测)都可能出现
257
+ const p6m = rcx.p6Mode || (rcx.run && rcx.run.p6Mode);
258
+ if (stageId === "P6" && (p6m === "session" || p6m === "claude")) return true;
259
+ // stageCfgOf 由 buildRcx 注入;裸 rcx(单测)回落"未开启委托"
260
+ const cfg = typeof rcx.stageCfgOf === "function" ? rcx.stageCfgOf(stageId) : null;
261
+ return !!(cfg && cfg.delegate && cfg.delegate.mode === "session");
262
+ }
263
+
264
+ export async function buildDelegateTask(rcx, stageId) {
265
+ const def = STAGE_DEFS[stageId];
266
+ const spec = def.delegateSpec;
267
+ const cfg = rcx.stageCfgOf(stageId);
268
+ const lines = [
269
+ `# ${stageId} ${def.name} · 外部智能体任务包`, "",
270
+ `- 阶段职责:${def.desc}`,
271
+ `- 输出产物:\`${spec.output}\`(就绪后人工通过复核门,流水线继续)`,
272
+ `- 输出契约:${spec.contract}`, "",
273
+ ];
274
+ // 输入:trigger=触发原文 / failure=失败信息 / 产物路径列表 / repo=本地仓库
275
+ const inputs = Array.isArray(spec.inputs) ? spec.inputs : [spec.inputs];
276
+ for (const inp of inputs) {
277
+ if (inp === "trigger") {
278
+ lines.push(`## 输入 · 触发原文`, "```markdown", String(await readTriggerText(rcx)).slice(0, 8000), "```", "");
279
+ } else if (inp === "failure") {
280
+ lines.push(`## 输入 · 失败信息`, "```json", JSON.stringify(rcx.failure || {}, null, 2), "```", "");
281
+ } else if (inp === "repo") {
282
+ lines.push(`## 输入 · 本地仓库`, `\`${rcx.repoDir}\`(在仓库中检索/阅读源码)`, "");
283
+ } else {
284
+ const text = readArtifact(rcx.runDir, inp);
285
+ lines.push(`## 输入 · ${inp}`, "```", String(text == null ? "(缺失)" : text).slice(0, 12000), "```", "");
286
+ }
287
+ }
288
+ if (rcx.reviewComment) lines.push(`## 打回意见(必须修正)`, rcx.reviewComment, "");
289
+ if (cfg.delegate.agent) lines.push(`## 指定智能体`, cfg.delegate.agent, "");
290
+ if (cfg.delegate.brief) lines.push(`## 附加要求(委托人填写)`, cfg.delegate.brief, "");
291
+ return lines.join("\n");
292
+ }
293
+
294
+ // 委托产出就绪判定(applyReview 拦截空产出放行用):
295
+ // P6 维持 patches/coder-report 口径;其余 = 阶段产物文件已落盘
296
+ export function delegateReady(runDir, stageId) {
297
+ if (stageId === "P6") {
298
+ const out = runDir + "/06-implementation";
299
+ return delegateReadyFromFile(runDir, "06-implementation/coder-report.json") ||
300
+ delegateReadyFromDir(runDir, "06-implementation/patches", ".diff");
301
+ }
302
+ const output = STAGE_DEFS[stageId]?.delegateSpec?.output;
303
+ if (!output) return true;
304
+ return delegateReadyFromFile(runDir, output);
305
+ }
306
+
307
+ function delegateReadyFromFile(runDir, rel) {
308
+ return readArtifact(runDir, rel) != null;
309
+ }
310
+ function delegateReadyFromDir(runDir, rel, ext) {
311
+ const dir = join(runDir, rel);
312
+ return existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(ext));
313
+ }
@@ -0,0 +1,127 @@
1
+ // lib/stages/helpers.js — 各阶段通用辅助(读触发文档 / 列仓库文件 / 读文件 / 取上游产物 / 过程事件 / 委托外部)
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { join, sep } from "node:path";
4
+ import { appendArtifactLine, writeArtifact } from "../store.js";
5
+ import { stageDelegated, buildDelegateTask } from "../stageConfig.js";
6
+ import { matchConnection } from "../connections.js";
7
+
8
+ // 过程事件:追加到 runDir/trace/events.jsonl(UI 阶段详情按 stage 过滤展示)。
9
+ // kind:stage | llm | git | test | tool | info;detail 截断 2000 字防膨胀。
10
+ export function logEvent(rcx, ev) {
11
+ if (!rcx || !rcx.runDir) return;
12
+ try {
13
+ appendArtifactLine(rcx.runDir, "trace/events.jsonl", {
14
+ at: new Date().toISOString(),
15
+ stage: (rcx.run && rcx.run.current) || null,
16
+ kind: ev.kind || "info",
17
+ name: String(ev.name || "").slice(0, 200),
18
+ detail: String(ev.detail == null ? "" : ev.detail).slice(0, 2000),
19
+ ms: typeof ev.ms === "number" ? Math.round(ev.ms) : null,
20
+ ok: ev.ok !== false,
21
+ });
22
+ } catch { /* 事件写盘失败不影响主流程 */ }
23
+ }
24
+
25
+ export async function readTriggerText(rcx) {
26
+ const uri = rcx.trigger.uri;
27
+ if (/^https?:\/\//.test(uri)) {
28
+ const conns = Array.isArray(rcx.connections) ? rcx.connections : [];
29
+ // GitHub issue → API 抓取标题+正文(公开仓库无需 token)。
30
+ // API 失败(私有仓库 404 / 限流 403)时直接报错——降级抓 HTML 页面只会给
31
+ // P1 喂进导航页噪音;让失败在发起 Run 时就可见、可定位。
32
+ const gh = uri.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
33
+ if (gh) {
34
+ const [, owner, repo, num] = gh;
35
+ // token 来源:项目页「Git 托管连接」的 github.com 连接优先,本机环境变量兜底;
36
+ // 匿名 API 限流仅 60 次/小时/IP,极易 403
37
+ const ghConn = matchConnection(uri, conns);
38
+ const ghToken = (ghConn && ghConn.kind === "github" && ghConn.token) || process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
39
+ let resp;
40
+ try {
41
+ resp = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${num}`, {
42
+ headers: {
43
+ Accept: "application/vnd.github+json",
44
+ "User-Agent": "dsh-issue2pr",
45
+ ...(ghToken ? { Authorization: "Bearer " + ghToken } : {}),
46
+ },
47
+ });
48
+ } catch (e) { throw new Error("GitHub API 请求失败: " + String((e && e.message) || e)); }
49
+ if (!resp.ok) {
50
+ const hint = resp.status === 404 ? "(仓库不存在或为私有仓库;私有仓库可在项目页「Git 托管连接」配置 GitHub 凭据,或改用本地导出的 issue 文档)"
51
+ : resp.status === 403 ? `(API 限流${ghToken ? "(含认证请求)" : ",可在项目页「Git 托管连接」配置 GitHub token(或设置 GITHUB_TOKEN 环境变量)提升额度"},稍后重试或改用本地文档)` : "";
52
+ throw new Error(`GitHub issue 获取失败 (HTTP ${resp.status})${hint}: ${uri}`);
53
+ }
54
+ const data = await resp.json();
55
+ return `# ${data.title || "(无标题)"}\n\n${data.body || "(无正文)"}`;
56
+ }
57
+ // GitLab issue(gitlab.com 或自建实例)→ /api/v4 抓取;私有仓库需「Git 托管连接」的 token
58
+ const gl = uri.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/-\/issues\/(\d+)/);
59
+ if (gl) {
60
+ const [, glHost, ns, name, glNum] = gl;
61
+ const glConn = matchConnection(uri, conns);
62
+ const glToken = (glConn && glConn.kind === "gitlab" && glConn.token) || "";
63
+ let resp;
64
+ try {
65
+ resp = await fetch(`https://${glHost}/api/v4/projects/${encodeURIComponent(ns + "/" + name)}/issues/${glNum}`, {
66
+ headers: { "User-Agent": "dsh-issue2pr", ...(glToken ? { Authorization: "Bearer " + glToken } : {}) },
67
+ });
68
+ } catch (e) { throw new Error("GitLab API 请求失败: " + String((e && e.message) || e)); }
69
+ if (!resp.ok) {
70
+ const hint = resp.status === 404 ? "(仓库不存在或无权限;可在项目页「Git 托管连接」配置该 GitLab 的 Access Token,或改用本地导出的 issue 文档)"
71
+ : (resp.status === 401 || resp.status === 403) ? "(凭据缺失或无效,可在项目页「Git 托管连接」配置该 GitLab 的 Access Token)" : "";
72
+ throw new Error(`GitLab issue 获取失败 (HTTP ${resp.status})${hint}: ${uri}`);
73
+ }
74
+ const data = await resp.json();
75
+ return `# ${data.title || "(无标题)"}\n\n${data.description || "(无正文)"}`;
76
+ }
77
+ // 通用 HTTP fetch
78
+ const resp = await fetch(uri, { headers: { "User-Agent": "dsh-issue2pr" } });
79
+ if (!resp.ok) throw new Error(`获取触发文档失败 (${resp.status}): ${uri}`);
80
+ return await resp.text();
81
+ }
82
+ if (!existsSync(uri)) throw new Error("触发文档不存在: " + uri);
83
+ return readFileSync(uri, "utf8");
84
+ }
85
+
86
+ const IGNORE = new Set([".git", "node_modules", "dist", ".next", "coverage"]);
87
+ export function listRepoFiles(repoDir, max = 400) {
88
+ const out = [];
89
+ (function walk(dir) {
90
+ if (out.length >= max || !existsSync(dir)) return;
91
+ for (const name of readdirSync(dir)) {
92
+ if (IGNORE.has(name)) continue;
93
+ const full = join(dir, name);
94
+ if (statSync(full).isDirectory()) walk(full);
95
+ else { out.push(full.slice(repoDir.length + 1).split(sep).join("/")); if (out.length >= max) return; }
96
+ }
97
+ })(repoDir);
98
+ return out;
99
+ }
100
+
101
+ export function readRepoFile(repoDir, rel, maxChars = 6000) {
102
+ // 防路径逃逸(Task 7 修复):含 .. 段或绝对路径的 rel 直接占位返回,不读仓库外文件
103
+ const relStr = String(rel);
104
+ if (relStr.split(/[\\/]+/).includes("..") || /^([a-zA-Z]:)?[\\/]/.test(relStr)) return "(非法路径)";
105
+ const full = join(repoDir, rel);
106
+ if (!existsSync(full)) return `<文件不存在: ${rel}>`;
107
+ return readFileSync(full, "utf8").slice(0, maxChars);
108
+ }
109
+
110
+ export function requireArtifact(runDir, rel, readArtifact) {
111
+ const text = readArtifact(runDir, rel);
112
+ if (text == null) throw new Error("缺少上游产物: " + rel);
113
+ return text;
114
+ }
115
+
116
+ // 委托外部智能体:开放委托的阶段在执行器开头调用;返回 external 结果(advance
117
+ // 据此停复核门等外部产出),未开启委托返回 null 走本机执行。
118
+ // P6 任务包路径沿用 06-implementation/session-task.md(与既有 UI 提示/存量 run 兼容)
119
+ export async function maybeDelegate(rcx, stageId) {
120
+ if (!stageDelegated(rcx, stageId)) return null;
121
+ const rel = stageId === "P6" ? "06-implementation/session-task.md" : `delegate/${stageId}-task.md`;
122
+ const task = await buildDelegateTask(rcx, stageId);
123
+ writeArtifact(rcx.runDir, rel, task);
124
+ logEvent(rcx, { kind: "info", name: stageId + " 委托外部智能体:任务包已生成",
125
+ detail: rel + " 已写入;外部智能体产出就绪后,回复核门通过继续" });
126
+ return { artifact: rel, summary: "任务包已生成,等待外部智能体执行", external: true };
127
+ }
@@ -0,0 +1,16 @@
1
+ // lib/stages/index.js — 阶段注册表(11 阶段齐全)
2
+ import p1 from "./p1-issue-analyzer.js";
3
+ import p2 from "./p2-search.js";
4
+ import p3 from "./p3-code-understanding.js";
5
+ import p4 from "./p4-hypothesis.js";
6
+ import p5 from "./p5-planner.js";
7
+ import p6 from "./p6-coder.js";
8
+ import p7 from "./p7-patch.js";
9
+ import p8 from "./p8-test-runner.js";
10
+ import p9 from "./p9-reviewer.js";
11
+ import p10 from "./p10-failure.js";
12
+ import p11 from "./p11-pr-builder.js";
13
+
14
+ export function buildExecutors() {
15
+ return { P1: p1, P2: p2, P3: p3, P4: p4, P5: p5, P6: p6, P7: p7, P8: p8, P9: p9, P10: p10, P11: p11 };
16
+ }
@@ -0,0 +1,18 @@
1
+ // lib/stages/p1-issue-analyzer.js — 调研 §3:自然语言 → 结构化契约
2
+ import { writeArtifact } from "../store.js";
3
+ import { readTriggerText, maybeDelegate } from "./helpers.js";
4
+ import { sysOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P1");
8
+ if (delegated) return delegated;
9
+ const issue = await readTriggerText(rcx);
10
+ const rejected = rcx.reviewComment ? `\n\n【人工复核打回意见,必须修正】${rcx.reviewComment}` : "";
11
+ const out = await rcx.llm.completeJson({
12
+ system: sysOf(rcx, "P1"),
13
+ user: `【Issue 全文】\n${issue}\n\n【输出契约】{"phenomenon":"现象","trigger":"触发条件","scope":["影响模块"],"success_criteria":["可验证成功标准"],"constraints":["约束"],"risk_level":"low|medium|high"}${rejected}`,
14
+ required: ["phenomenon", "trigger", "scope", "success_criteria", "constraints", "risk_level"],
15
+ });
16
+ writeArtifact(rcx.runDir, "01-issue-analysis.json", JSON.stringify(out, null, 2));
17
+ return { artifact: "01-issue-analysis.json", summary: `scope=${(out.scope || []).join(",")}` };
18
+ }
@@ -0,0 +1,21 @@
1
+ // lib/stages/p10-failure.js — 调研 §10:先分类,再决定路径
2
+ import { writeArtifact } from "../store.js";
3
+ import { maybeDelegate } from "./helpers.js";
4
+ import { sysOf } from "../stageConfig.js";
5
+
6
+ const CATEGORIES = ["实现错误", "根因错误", "测试选择", "环境缺失", "权限被拒", "反复失败"];
7
+
8
+ export default async function execute(rcx) {
9
+ // 仅失败路径动作:run.status 非 failed 直接跳过(不写任何产物)
10
+ if (rcx.run?.status !== "failed") return { artifact: null, summary: "非失败路径,P10 跳过" };
11
+ const delegated = await maybeDelegate(rcx, "P10");
12
+ if (delegated) return delegated;
13
+ const out = await rcx.llm.completeJson({
14
+ system: sysOf(rcx, "P10"),
15
+ user: `【失败阶段】${rcx.failure?.stage}\n【错误】${rcx.failure?.error}\n【类别】${CATEGORIES.join("/")}\n【输出契约】{"category":"类别","detail":"依据","action":"replan|rollback|escalate"}`,
16
+ required: ["category", "action"],
17
+ });
18
+ if (!CATEGORIES.includes(out.category)) out.category = "反复失败";
19
+ writeArtifact(rcx.runDir, "09-failure-analysis.json", JSON.stringify(out, null, 2));
20
+ return { artifact: "09-failure-analysis.json", summary: `${out.category}→${out.action}` };
21
+ }
@@ -0,0 +1,24 @@
1
+ // lib/stages/p11-pr-builder.js — 调研 §15:PR 说明忠实反映修改 + Gate 评测
2
+ import { writeArtifact, readArtifact } from "../store.js";
3
+ import { requireArtifact, maybeDelegate } from "./helpers.js";
4
+ import { sysOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P11");
8
+ if (delegated) return delegated;
9
+ const issue = requireArtifact(rcx.runDir, "01-issue-analysis.json", readArtifact);
10
+ const review = requireArtifact(rcx.runDir, "08-review-report.json", readArtifact);
11
+ const tests = requireArtifact(rcx.runDir, "07-test-report.json", readArtifact);
12
+ const md = await rcx.llm.complete({
13
+ system: sysOf(rcx, "P11", "desc"),
14
+ user: `【Issue 契约】\n${issue}\n【Reviewer 结论】\n${review}\n【测试报告】\n${tests}`,
15
+ });
16
+ writeArtifact(rcx.runDir, "10-pr-description.md", md);
17
+ const evalOut = await rcx.llm.completeJson({
18
+ system: sysOf(rcx, "P11", "gate"),
19
+ user: `【PR 说明】\n${md}\n【输出契约】{"ROOT":"pass|fail","PATCH":"pass|fail","TEST":"pass|fail","DIFF":"pass|fail","DESC":"pass|fail","ACCEPT":"pass|fail"}`,
20
+ required: ["ROOT", "PATCH", "TEST", "DIFF", "DESC", "ACCEPT"],
21
+ });
22
+ writeArtifact(rcx.runDir, "11-eval-report.json", JSON.stringify(evalOut, null, 2));
23
+ return { artifact: "10-pr-description.md", summary: "PR 说明 + Gate 评测完成" };
24
+ }
@@ -0,0 +1,18 @@
1
+ // lib/stages/p2-search.js — 调研 §4:结构化 Issue → 候选文件 + 证据
2
+ import { writeArtifact, readArtifact } from "../store.js";
3
+ import { listRepoFiles, requireArtifact, maybeDelegate } from "./helpers.js";
4
+ import { sysOf, paramsOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P2");
8
+ if (delegated) return delegated;
9
+ const contract = requireArtifact(rcx.runDir, "01-issue-analysis.json", readArtifact);
10
+ const files = listRepoFiles(rcx.repoDir, paramsOf(rcx, "P2").repoScanMax).join("\n");
11
+ const out = await rcx.llm.completeJson({
12
+ system: sysOf(rcx, "P2"),
13
+ user: `【Issue 契约】\n${contract}\n\n【仓库文件清单】\n${files}\n\n【输出契约】{"candidates":[{"path":"相对路径","role":"模块角色","evidence":"选择理由","confidence":"high|medium|low"}],"test_candidates":["测试文件"],"uncertain":["需进一步探索项"]}`,
14
+ required: ["candidates", "test_candidates", "uncertain"],
15
+ });
16
+ writeArtifact(rcx.runDir, "02-search-candidates.json", JSON.stringify(out, null, 2));
17
+ return { artifact: "02-search-candidates.json", summary: `候选 ${out.candidates.length} 个` };
18
+ }
@@ -0,0 +1,24 @@
1
+ // lib/stages/p3-code-understanding.js — 调研 §4:调用链理解(基于真实文件内容)
2
+ import { writeArtifact, readArtifact } from "../store.js";
3
+ import { requireArtifact, readRepoFile, maybeDelegate } from "./helpers.js";
4
+ import { sysOf, paramsOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P3");
8
+ if (delegated) return delegated;
9
+ const candidates = JSON.parse(requireArtifact(rcx.runDir, "02-search-candidates.json", readArtifact));
10
+ const pm = paramsOf(rcx, "P3");
11
+ const files = (candidates.candidates || []).slice(0, pm.deepReadFiles)
12
+ .map((c) => {
13
+ const raw = readRepoFile(rcx.repoDir, c.path, pm.fileChars);
14
+ // 行号前缀让 LLM 能给出可信的 `路径:行号` 锚点(readRepoFile 本身不加,P6 写补丁依赖原文)
15
+ const numbered = raw.split("\n").map((l, i) => `${i + 1}|${l}`).join("\n");
16
+ return `### ${c.path}\n\`\`\`\n${numbered}\n\`\`\``;
17
+ }).join("\n\n");
18
+ const md = await rcx.llm.complete({
19
+ system: sysOf(rcx, "P3"),
20
+ user: `【候选文件内容】(每行前缀「行号|」为源码真实行号)\n${files}\n\n【打回意见】${rcx.reviewComment || "无"}`,
21
+ });
22
+ writeArtifact(rcx.runDir, "03-code-understanding.md", md);
23
+ return { artifact: "03-code-understanding.md", summary: "报告 " + md.length + " 字" };
24
+ }
@@ -0,0 +1,17 @@
1
+ // lib/stages/p4-hypothesis.js — 调研 §5:每个假设必须可验证
2
+ import { writeArtifact, readArtifact } from "../store.js";
3
+ import { requireArtifact, maybeDelegate } from "./helpers.js";
4
+ import { sysOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P4");
8
+ if (delegated) return delegated;
9
+ const report = requireArtifact(rcx.runDir, "03-code-understanding.md", readArtifact);
10
+ const out = await rcx.llm.completeJson({
11
+ system: sysOf(rcx, "P4"),
12
+ user: `【代码理解报告】\n${report}\n\n【输出契约】{"hypotheses":[{"id":"A","title":"假设","evidence":"来自报告的证据","verify_file":"验证文件","verify_method":"可执行的验证方法"}]}\n【打回意见】${rcx.reviewComment || "无"}`,
13
+ required: ["hypotheses"],
14
+ });
15
+ writeArtifact(rcx.runDir, "04-hypotheses.json", JSON.stringify(out, null, 2));
16
+ return { artifact: "04-hypotheses.json", summary: `假设 ${out.hypotheses.length} 个` };
17
+ }
@@ -0,0 +1,17 @@
1
+ // lib/stages/p5-planner.js — 调研 §6:TaskGraph,每节点有契约
2
+ import { writeArtifact, readArtifact } from "../store.js";
3
+ import { requireArtifact, maybeDelegate } from "./helpers.js";
4
+ import { sysOf } from "../stageConfig.js";
5
+
6
+ export default async function execute(rcx) {
7
+ const delegated = await maybeDelegate(rcx, "P5");
8
+ if (delegated) return delegated;
9
+ const hypotheses = requireArtifact(rcx.runDir, "04-hypotheses.json", readArtifact);
10
+ const out = await rcx.llm.completeJson({
11
+ system: sysOf(rcx, "P5"),
12
+ user: `【根因假设】\n${hypotheses}\n\n【输出契约】{"nodes":[{"id":"T1","title":"任务","input":"前置 artifact","output":"本任务 artifact","deps":["T0"],"success_criteria":"可验证通过条件","risk":"low|medium|high"}],"review_gate":"节点id","pr_gate":"节点id"}\n【打回意见】${rcx.reviewComment || "无"}`,
13
+ required: ["nodes"],
14
+ });
15
+ writeArtifact(rcx.runDir, "05-task-graph.json", JSON.stringify(out, null, 2));
16
+ return { artifact: "05-task-graph.json", summary: `${out.nodes.length} 节点` };
17
+ }