dsh-issue2pr 0.3.0 → 0.3.1
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 +16 -12
- package/client.js +3701 -3617
- package/index.js +862 -721
- package/lib/assistant.js +3 -3
- package/lib/{pipeline.js → core/pipeline.js} +55 -4
- package/lib/{stageConfig.js → core/stageConfig.js} +24 -2
- package/lib/{agents.js → delegate/agents.js} +1 -1
- package/lib/delegate/delegateVerify.js +125 -0
- package/lib/infra/repoState.js +59 -0
- package/lib/stages/helpers.js +3 -3
- package/lib/stages/p1-issue-analyzer.js +2 -2
- package/lib/stages/p10-failure.js +2 -2
- package/lib/stages/p11-pr-builder.js +2 -2
- package/lib/stages/p2-search.js +2 -2
- package/lib/stages/p3-code-understanding.js +2 -2
- package/lib/stages/p4-hypothesis.js +2 -2
- package/lib/stages/p5-planner.js +2 -2
- package/lib/stages/p6-coder.js +4 -4
- package/lib/stages/p7-patch.js +12 -3
- package/lib/stages/p8-test-runner.js +2 -2
- package/lib/stages/p9-reviewer.js +2 -2
- package/package.json +1 -1
- /package/lib/{store.js → core/store.js} +0 -0
- /package/lib/{connections.js → infra/connections.js} +0 -0
- /package/lib/{llm.js → infra/llm.js} +0 -0
package/lib/assistant.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// focus 来自前端 viewStore:{ nav, slug, runId }(用户当前所在页面 / 选中项目 / 选中 Run)
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { listProjects, readArtifact } from "./store.js";
|
|
6
|
-
import { loadRun } from "./pipeline.js";
|
|
7
|
-
import { STAGE_DEFS } from "./stageConfig.js";
|
|
5
|
+
import { listProjects, readArtifact } from "./core/store.js";
|
|
6
|
+
import { loadRun } from "./core/pipeline.js";
|
|
7
|
+
import { STAGE_DEFS } from "./core/stageConfig.js";
|
|
8
8
|
|
|
9
9
|
export const ASSISTANT_SYSTEM_HEAD = [
|
|
10
10
|
"你是 Issue2PR 插件(运行在 DSH 宿主里)内置的智能助手,悬浮在工作台右上角,",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { writeArtifact, appendArtifactLine, timestamp } from "./store.js";
|
|
5
|
-
import { STAGE_DEFS, stageDelegated, delegateReady } from "./stageConfig.js";
|
|
5
|
+
import { STAGE_DEFS, stageDelegated, delegateReady, purgeDelegateArtifacts } from "./stageConfig.js";
|
|
6
|
+
import { verifyDelegateResult } from "../delegate/delegateVerify.js";
|
|
6
7
|
|
|
7
8
|
export const STAGES = [
|
|
8
9
|
{ id: "P1", name: "IssueAnalyzer", artifact: "01-issue-analysis.json", key: false },
|
|
@@ -84,6 +85,18 @@ export async function advance(rcx) {
|
|
|
84
85
|
appendEvent(runDir, nextId, { kind: "stage", name: nextId + " 开始", detail: STAGES.find((s) => s.id === nextId)?.name || "" });
|
|
85
86
|
const t0 = Date.now();
|
|
86
87
|
try {
|
|
88
|
+
// A4 兜底 + 修正:P7 应用前对委托 P6 产物做完整验证(不只是存在性检查)。
|
|
89
|
+
// 存在 ≠ 可用:产物被外部删除 → 显式失败;产物存在但对 HEAD 基线不可应用(过期/坏补丁)
|
|
90
|
+
// → 同样显式拦截,而不是让 git apply 在半路炸出难定位的错。
|
|
91
|
+
if (nextId === "P7" && stageDelegated(rcx, "P6")) {
|
|
92
|
+
if (!delegateReady(runDir, "P6")) {
|
|
93
|
+
throw new Error("P6 委托产物缺失(无 coder-report.json / patches/*.diff),P7 无补丁可应用;请回退 P6 重跑");
|
|
94
|
+
}
|
|
95
|
+
const v7 = await verifyDelegateResult(rcx, "P6");
|
|
96
|
+
if (!v7.ok) {
|
|
97
|
+
throw new Error("P6 委外产物验证未通过,P7 拒绝应用: " + v7.errors.join(";"));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
87
100
|
const result = await executors[nextId](rcx);
|
|
88
101
|
// 执行器运行期间可能被外部 stop/delete:落盘前复查,避免把 running 写回覆盖 stopped
|
|
89
102
|
const diskAfter = loadRun(runDir);
|
|
@@ -95,12 +108,33 @@ export async function advance(rcx) {
|
|
|
95
108
|
if (result && result.external) st.external = true; // session 模式:实施移交外部会话
|
|
96
109
|
st.finishedAt = new Date().toISOString();
|
|
97
110
|
appendSpan(runDir, { span: nextId, ms: Date.now() - t0, decision: (result && result.summary) || "" });
|
|
98
|
-
|
|
111
|
+
// A4 修复 + 修正:委托阶段必须「拿到委外结果且验证 ok」才允许流转,缺一不可:
|
|
112
|
+
// ① 产物未就绪 → 无论复核模式是否有门都停下等待(空手放行 = 下游必然失败);
|
|
113
|
+
// ② 产物就绪 → 立即执行验证(结构完整 + 对 HEAD 基线的应用性演练),验证不过 = 显式失败。
|
|
114
|
+
// 全自动模式 claude 产出的坏补丁 / 会话期间被篡改的补丁,原先会直通 P7 才在 apply 炸,
|
|
115
|
+
// 现在在 P6 门上就地拦截并给出可定位的错误。
|
|
116
|
+
let delegateBlocked = false;
|
|
117
|
+
if (stageDelegated(rcx, nextId)) {
|
|
118
|
+
if (!delegateReady(runDir, nextId)) {
|
|
119
|
+
delegateBlocked = true;
|
|
120
|
+
} else {
|
|
121
|
+
const v = await verifyDelegateResult(rcx, nextId);
|
|
122
|
+
appendEvent(runDir, nextId, { kind: "stage", name: nextId + " 委外产物验证" + (v.ok ? "通过" : "未通过"),
|
|
123
|
+
detail: v.ok
|
|
124
|
+
? "结构完整" + (v.rehearsal ? ",对 HEAD 基线应用性演练通过(" + v.patches + " 份补丁)" : "(无仓库环境,未演练)")
|
|
125
|
+
: v.errors.join(";"),
|
|
126
|
+
ok: v.ok });
|
|
127
|
+
if (!v.ok) throw new Error(nextId + " 委外产物验证未通过: " + v.errors.join(";") + ";请回退该阶段重跑,或修正外部产物");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
st.status = (isGate(run, nextId) || delegateBlocked) ? "awaiting_review" : "approved";
|
|
99
131
|
run.status = st.status === "approved" ? "running" : "awaiting_review";
|
|
100
132
|
// external 阶段的实施不在本阶段完成,事件文案不能写"完成"(避免误导为已交付)
|
|
101
133
|
const doneLabel = result && result.external
|
|
102
134
|
? nextId + " 任务包已生成 · 等待外部会话执行"
|
|
103
|
-
:
|
|
135
|
+
: delegateBlocked
|
|
136
|
+
? nextId + " 任务包已生成 · 等待外部产出后放行"
|
|
137
|
+
: nextId + (st.status === "awaiting_review" ? " 完成 · 待复核" : " 完成");
|
|
104
138
|
appendEvent(runDir, nextId, { kind: "stage", name: doneLabel,
|
|
105
139
|
detail: (result && result.summary) || "", ms: Date.now() - t0 });
|
|
106
140
|
} catch (e) {
|
|
@@ -116,7 +150,9 @@ export async function advance(rcx) {
|
|
|
116
150
|
}
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
// 异步原因(A4 修正):委托阶段的通过判定要做完整验证(含 git 应用性演练),
|
|
154
|
+
// 文件存在性检查不再足够——人工放行与全自动放行同一验证口径。
|
|
155
|
+
export async function applyReview(rcx, { decision, comment }) {
|
|
120
156
|
const { run, runDir } = rcx;
|
|
121
157
|
const id = run.current;
|
|
122
158
|
const st = run.stages[id];
|
|
@@ -132,6 +168,14 @@ export function applyReview(rcx, { decision, comment }) {
|
|
|
132
168
|
const mode = isP6 ? ((run.p6Mode || "session") + " 模式") : "委托模式";
|
|
133
169
|
return [false, id + " " + mode + ":外部执行方尚未产出(未检测到 " + output + ")。请先在外部智能体执行任务包,产出落盘后再通过复核门"];
|
|
134
170
|
}
|
|
171
|
+
// A4 修正:人工放行同样必须先过机器验证——「文件存在」只证明拿到结果,不证明结果可用。
|
|
172
|
+
// 坏补丁(结构残缺 / 对 HEAD 基线不可应用 / report 损坏)在这里拦截,不让它进 P7。
|
|
173
|
+
if (decision === "approve" && stageDelegated(rcx, id)) {
|
|
174
|
+
const v = await verifyDelegateResult(rcx, id);
|
|
175
|
+
if (!v.ok) {
|
|
176
|
+
return [false, id + " 委外产物验证未通过: " + v.errors.join(";") + "。请修正外部产物后重试,或打回重跑"];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
135
179
|
const record = { stage: id, decision, comment: comment || "", at: new Date().toISOString() };
|
|
136
180
|
writeArtifact(runDir, `reviews/${timestamp()}-${decision}-${id}.json`, JSON.stringify(record, null, 2));
|
|
137
181
|
appendEvent(runDir, id, { kind: "stage", name: id + (decision === "approve" ? " 复核通过" : " 复核打回"),
|
|
@@ -145,6 +189,13 @@ export function applyReview(rcx, { decision, comment }) {
|
|
|
145
189
|
st.attempts += 1;
|
|
146
190
|
run.status = "running";
|
|
147
191
|
rcx.reviewComment = comment;
|
|
192
|
+
// A2 修复:打回委托阶段时清空旧外部产物 —— 旧 patches/report 会让 delegateReady 误判
|
|
193
|
+
// "外部已交付"(复核门形同虚设),并被 P7 当作本轮补丁应用(过期 patch 混入)。清场后按新任务包重新产出。
|
|
194
|
+
if (stageDelegated(rcx, id)) {
|
|
195
|
+
const removed = purgeDelegateArtifacts(runDir, id);
|
|
196
|
+
appendEvent(runDir, id, { kind: "stage", name: id + " 打回清场",
|
|
197
|
+
detail: removed.length ? "已清除旧外部产物: " + removed.join(", ") : "无旧外部产物可清" });
|
|
198
|
+
}
|
|
148
199
|
}
|
|
149
200
|
saveRun(runDir, run);
|
|
150
201
|
return [true, decision === "approve" ? "已通过" : "已打回,将带意见重跑"];
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// 配置存于 project.json 的 stageConfig 字段:{ P1: { prompts, provider, model,
|
|
3
3
|
// reasoningEffort, timeoutMs, maxTokens, delegate }, … };未配置项回落默认值。
|
|
4
4
|
// STAGE_DEFS 的 prompts 与各阶段文件中的硬编码提示词保持 1:1(改默认值两处同步改)。
|
|
5
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { readArtifact } from "./store.js";
|
|
8
|
-
import { readTriggerText } from "
|
|
8
|
+
import { readTriggerText } from "../stages/helpers.js";
|
|
9
9
|
|
|
10
10
|
const CODER_SYSTEM = [
|
|
11
11
|
"你是 Coder Sub-Agent,纪律(superpowers):",
|
|
@@ -311,3 +311,25 @@ function delegateReadyFromDir(runDir, rel, ext) {
|
|
|
311
311
|
const dir = join(runDir, rel);
|
|
312
312
|
return existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(ext));
|
|
313
313
|
}
|
|
314
|
+
|
|
315
|
+
// 清空委托阶段的外部产出(打回 / 回退重跑时调用,返回被清除的产物相对路径清单)。
|
|
316
|
+
// 不清理旧产物的后果:delegateReady 误判"外部已交付"(复核门形同虚设),
|
|
317
|
+
// P7 把过期 patch 当作本轮补丁应用(打回意见等于没有生效)。任务包(session-task.md 等)保留,
|
|
318
|
+
// 由阶段执行器重跑时重新生成。
|
|
319
|
+
export function purgeDelegateArtifacts(runDir, stageId) {
|
|
320
|
+
const removed = [];
|
|
321
|
+
const kill = (rel) => {
|
|
322
|
+
try { rmSync(join(runDir, rel), { force: true, recursive: true }); removed.push(rel); } catch { /* 尽力 */ }
|
|
323
|
+
};
|
|
324
|
+
if (stageId === "P6") {
|
|
325
|
+
kill("06-implementation/coder-report.json");
|
|
326
|
+
const dir = join(runDir, "06-implementation", "patches");
|
|
327
|
+
if (existsSync(dir)) {
|
|
328
|
+
for (const f of readdirSync(dir)) if (f.endsWith(".diff")) kill("06-implementation/patches/" + f);
|
|
329
|
+
}
|
|
330
|
+
return removed;
|
|
331
|
+
}
|
|
332
|
+
const output = STAGE_DEFS[stageId]?.delegateSpec?.output;
|
|
333
|
+
if (output && !output.endsWith("/")) kill(output); // 目录型产物仅 P6;其余委托阶段产物为文件
|
|
334
|
+
return removed;
|
|
335
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 全部探测函数可注入(index.js 传 __testHooks 版本),单测不依赖本机 claude。
|
|
7
7
|
import { execFile, spawn } from "node:child_process";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { resolveClaudeBin, claudeCommonCandidates } from "
|
|
9
|
+
import { resolveClaudeBin, claudeCommonCandidates } from "../stages/p6-coder.js";
|
|
10
10
|
|
|
11
11
|
export const AGENT_SOURCE_LABELS = {
|
|
12
12
|
configured: "项目配置",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// lib/delegateVerify.js — 委外产物验证(审查报告 A4 修正:拿到结果 + 验证 ok 才往下流转)
|
|
2
|
+
// 设计原则(第一性):流水线放行一个委托阶段前,唯一可信的依据不是"文件存在",
|
|
3
|
+
// 而是"产物可用"——即下游阶段拿它当输入时不会必然失败。因此验证分两层:
|
|
4
|
+
// 1) 结构完整(拿到结果):补丁清单与 P7 完全同口径(collectPatches),逐份存在、
|
|
5
|
+
// 非空、形如 unified diff;coder-report.json 若存在必须可解析。
|
|
6
|
+
// 2) 可应用性演练(验证 ok):用临时 GIT_INDEX_FILE 从 HEAD 构建一次性索引,
|
|
7
|
+
// 按应用序逐份 `git apply --cached` 演练——不碰工作区、不依赖工作区是否干净,
|
|
8
|
+
// 语义与 P7(reset 到 HEAD 后顺序 apply)完全一致。
|
|
9
|
+
// 无 repoDir(纯单测裸 rcx)时退化为仅结构验证;repoDir 存在但非 git 仓库 → 显式验证失败。
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { readArtifact } from "../core/store.js";
|
|
15
|
+
import { STAGE_DEFS } from "../core/stageConfig.js";
|
|
16
|
+
import { collectPatches } from "../stages/p7-patch.js";
|
|
17
|
+
|
|
18
|
+
const firstLine = (s) => String(s || "").split("\n").find((l) => l.trim()) || "";
|
|
19
|
+
|
|
20
|
+
function execGit(args, { cwd, env, input }) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = execFile("git", args, {
|
|
23
|
+
cwd, env,
|
|
24
|
+
timeout: 60000, windowsHide: true, maxBuffer: 16 * 1024 * 1024,
|
|
25
|
+
}, (err, stdout, stderr) => {
|
|
26
|
+
if (err) { err.stderr = String(stderr || err.message); reject(err); }
|
|
27
|
+
else resolve(String(stdout || ""));
|
|
28
|
+
});
|
|
29
|
+
child.stdin?.on?.("error", () => {}); // EPIPE 不掩盖主错误
|
|
30
|
+
child.stdin.end(input == null ? "" : input); // execFile 无 input 选项,须手写 stdin
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 形如 unified diff:git 风格(diff --git)或 patch 风格(--- / +++ 头)
|
|
35
|
+
function looksLikeDiff(text) {
|
|
36
|
+
const t = String(text);
|
|
37
|
+
return /^diff --git /m.test(t) || (/^--- /m.test(t) && /^\+\+\+ /m.test(t));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// —— P6 深验证:结构 +(有仓库时)应用性演练 ——
|
|
41
|
+
async function verifyP6(rcx) {
|
|
42
|
+
const { runDir } = rcx;
|
|
43
|
+
const errors = [];
|
|
44
|
+
// coder-report.json 存在时必须可解析(先于清单解析:坏 JSON 在此拦截并给出可定位错误,
|
|
45
|
+
// 而不是让 collectPatches/P7 在 JSON.parse 处崩出裸 SyntaxError)
|
|
46
|
+
const reportPath = join(runDir, "06-implementation", "coder-report.json");
|
|
47
|
+
if (existsSync(reportPath)) {
|
|
48
|
+
try { JSON.parse(readFileSync(reportPath, "utf8")); }
|
|
49
|
+
catch (e) { errors.push("coder-report.json 不是合法 JSON: " + String((e && e.message) || e)); }
|
|
50
|
+
}
|
|
51
|
+
const patches = errors.length ? [] : collectPatches(runDir); // 与 P7 应用口径 1:1:验证的就是 P7 将要应用的
|
|
52
|
+
if (!patches.length) errors.push("未检测到补丁(coder-report.json 未列出补丁且 patches/*.diff 为空)——未拿到委外结果");
|
|
53
|
+
|
|
54
|
+
const contents = [];
|
|
55
|
+
for (const p of patches) {
|
|
56
|
+
const text = readArtifact(runDir, p.patch);
|
|
57
|
+
if (text == null) { errors.push(p.patch + " 不存在(coder-report 清单与实际文件不一致)"); contents.push(null); continue; }
|
|
58
|
+
if (!String(text).trim()) { errors.push(p.patch + " 内容为空"); contents.push(null); continue; }
|
|
59
|
+
if (!looksLikeDiff(text)) { errors.push(p.patch + " 不是 unified diff(缺 ---/+++ 或 diff --git 头)"); contents.push(null); continue; }
|
|
60
|
+
contents.push(String(text));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 应用性演练:仅在结构完好时执行(缺文件的演练只会产生噪音)
|
|
64
|
+
let rehearsal = false;
|
|
65
|
+
if (!errors.length && patches.length) {
|
|
66
|
+
if (!rcx.repoDir || !existsSync(rcx.repoDir)) {
|
|
67
|
+
// 无仓库环境(纯单测 / 测试钩子跳过仓库准备):仅结构验证。
|
|
68
|
+
// 生产路径 drive 在 P2-P6 前必保仓库就位;仓库目录缺失时由 P7 的
|
|
69
|
+
// resetRepoClean 显式失败兜底,此处不重复拦截。
|
|
70
|
+
} else {
|
|
71
|
+
rehearsal = true;
|
|
72
|
+
const r = await rehearseApply(rcx.repoDir, patches.map((p, i) => ({ rel: p.patch, content: contents[i] })));
|
|
73
|
+
if (!r.ok) errors.push(...r.errors);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { ok: !errors.length, errors, patches: patches.length, rehearsal };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// —— 临时索引演练:read-tree HEAD → 顺序 apply --cached(后续补丁在前序之上验证)——
|
|
80
|
+
async function rehearseApply(repoDir, patches) {
|
|
81
|
+
let tmp = null;
|
|
82
|
+
try {
|
|
83
|
+
try { await execGit(["rev-parse", "--git-dir"], { cwd: repoDir }); }
|
|
84
|
+
catch (e) { return { ok: false, errors: ["无法执行应用性演练(repoDir 非 git 仓库): " + firstLine(e.stderr || e.message)] }; }
|
|
85
|
+
tmp = mkdtempSync(join(tmpdir(), "i2p-verify-"));
|
|
86
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(tmp, "index") };
|
|
87
|
+
await execGit(["read-tree", "HEAD"], { cwd: repoDir, env }); // 一次性索引 = HEAD 基线
|
|
88
|
+
const errors = [];
|
|
89
|
+
for (const p of patches) {
|
|
90
|
+
try {
|
|
91
|
+
// 临时索引可丢弃:直接 apply 即检查(成功即变更索引,后续补丁在前序之上演练,
|
|
92
|
+
// 与 P7 reset→顺序 apply 的真实语义一致)
|
|
93
|
+
await execGit(["apply", "--cached", "-"], { cwd: repoDir, env, input: p.content });
|
|
94
|
+
} catch (e) {
|
|
95
|
+
errors.push(p.rel + " 无法应用到 HEAD 基线: " + firstLine(e.stderr || e.message));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { ok: !errors.length, errors };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { ok: false, errors: ["补丁应用性演练失败: " + String((e && e.stderr) || (e && e.message) || e)] };
|
|
101
|
+
} finally {
|
|
102
|
+
if (tmp) { try { rmSync(tmp, { recursive: true, force: true }); } catch { /* 尽力 */ } }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// —— 其余委托阶段轻验证:产物存在、非空;.json 契约必须可解析 ——
|
|
107
|
+
function verifyFileStage(rcx, stageId) {
|
|
108
|
+
const output = STAGE_DEFS[stageId]?.delegateSpec?.output;
|
|
109
|
+
if (!output || output.endsWith("/")) return { ok: true, errors: [], patches: 0, rehearsal: false };
|
|
110
|
+
const errors = [];
|
|
111
|
+
const text = readArtifact(rcx.runDir, output);
|
|
112
|
+
if (text == null) errors.push(output + " 尚未产出——未拿到委外结果");
|
|
113
|
+
else if (!String(text).trim()) errors.push(output + " 内容为空");
|
|
114
|
+
else if (output.endsWith(".json")) {
|
|
115
|
+
try { JSON.parse(String(text)); }
|
|
116
|
+
catch (e) { errors.push(output + " 不是合法 JSON: " + String((e && e.message) || e)); }
|
|
117
|
+
}
|
|
118
|
+
return { ok: !errors.length, errors, patches: text == null ? 0 : 1, rehearsal: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 入口:委托阶段产物验证。返回 { ok, errors[], patches, rehearsal }
|
|
122
|
+
// 调用方(applyReview / advance / 全自动 watcher / P7 兜底)必须 ok===true 才放行流转。
|
|
123
|
+
export async function verifyDelegateResult(rcx, stageId) {
|
|
124
|
+
return stageId === "P6" ? await verifyP6(rcx) : verifyFileStage(rcx, stageId);
|
|
125
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// lib/repoState.js — 仓库工作区基线与 per-Run worktree 管理(审查报告 A1/A3 修复)
|
|
2
|
+
// 目录职责:
|
|
3
|
+
// projects/<slug>/repo 基线克隆(git clone --depth 1 一次,作为只读基准)
|
|
4
|
+
// projects/<slug>/worktrees/<id> 每 Run 独立 worktree(P7 应用补丁 / P8 测试 / 外部智能体都在这里工作)
|
|
5
|
+
// per-Run worktree 让同项目并发 Run 的仓库状态互不污染;Run 删除时一并移除。
|
|
6
|
+
// worktree 创建失败时兜底回退基线 repo(配合 drive 的项目级互斥,仍保证不交错写)。
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
function execGit(args, cwd, timeoutMs = 120000) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
execFile("git", args, { cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
14
|
+
if (err) { err.stderr = String(stderr || err.message); reject(err); }
|
|
15
|
+
else resolve(String(stdout || ""));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function baseRepoDir(root, slug) { return join(root, "projects", slug, "repo"); }
|
|
21
|
+
export function runRepoDir(root, slug, runId) { return join(root, "projects", slug, "worktrees", runId); }
|
|
22
|
+
|
|
23
|
+
// 工作区重置回 HEAD 基线:清掉已应用补丁(跟踪文件修改)与未跟踪文件。
|
|
24
|
+
// 不加 -x:保留 .gitignore 忽略的目录(如 node_modules,避免测试依赖反复安装)。
|
|
25
|
+
export async function resetRepoClean(repoDir) {
|
|
26
|
+
try {
|
|
27
|
+
await execGit(["reset", "--hard", "HEAD"], repoDir, 60000);
|
|
28
|
+
await execGit(["clean", "-fd"], repoDir, 60000);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
throw new Error("仓库基线重置失败(git reset/clean): " + String((e && e.stderr) || (e && e.message) || e));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 确保 per-Run worktree 存在(幂等)。返回实际使用的仓库目录:
|
|
35
|
+
// 成功 → worktree 路径;基线仓库未就绪 / 创建失败 → 兜底返回基线 repo 路径(绝不抛错,由调用方日志呈现)。
|
|
36
|
+
export async function ensureWorktree(root, slug, runId, log) {
|
|
37
|
+
const base = baseRepoDir(root, slug);
|
|
38
|
+
const wt = runRepoDir(root, slug, runId);
|
|
39
|
+
if (existsSync(join(wt, ".git"))) return wt; // worktree 的 .git 是文件,existsSync 同样命中
|
|
40
|
+
if (!existsSync(join(base, ".git"))) return base; // 基线尚未克隆(克隆失败路径由调用方处理)
|
|
41
|
+
try {
|
|
42
|
+
await execGit(["worktree", "add", "--detach", wt, "HEAD"], base);
|
|
43
|
+
log?.({ kind: "git", name: "git worktree add", detail: "Run 独立工作区: " + wt });
|
|
44
|
+
return wt;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
log?.({ kind: "git", name: "git worktree add 失败(兜底共用基线仓库)", ok: false,
|
|
47
|
+
detail: String((e && e.stderr) || (e && e.message) || e).slice(0, 500) });
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Run 删除时尽力移除其 worktree(失败不抛:孤儿 worktree 可由 git worktree prune 收敛)
|
|
53
|
+
export async function removeWorktree(root, slug, runId) {
|
|
54
|
+
const base = baseRepoDir(root, slug);
|
|
55
|
+
const wt = runRepoDir(root, slug, runId);
|
|
56
|
+
if (!existsSync(wt) || !existsSync(join(base, ".git"))) return;
|
|
57
|
+
try { await execGit(["worktree", "remove", "--force", wt], base); } catch { /* 尽力 */ }
|
|
58
|
+
try { await execGit(["worktree", "prune"], base); } catch { /* 尽力 */ }
|
|
59
|
+
}
|
package/lib/stages/helpers.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// lib/stages/helpers.js — 各阶段通用辅助(读触发文档 / 列仓库文件 / 读文件 / 取上游产物 / 过程事件 / 委托外部)
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
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";
|
|
4
|
+
import { appendArtifactLine, writeArtifact } from "../core/store.js";
|
|
5
|
+
import { stageDelegated, buildDelegateTask } from "../core/stageConfig.js";
|
|
6
|
+
import { matchConnection } from "../infra/connections.js";
|
|
7
7
|
|
|
8
8
|
// 过程事件:追加到 runDir/trace/events.jsonl(UI 阶段详情按 stage 过滤展示)。
|
|
9
9
|
// kind:stage | llm | git | test | tool | info;detail 截断 2000 字防膨胀。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p1-issue-analyzer.js — 调研 §3:自然语言 → 结构化契约
|
|
2
|
-
import { writeArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact } from "../core/store.js";
|
|
3
3
|
import { readTriggerText, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P1");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p10-failure.js — 调研 §10:先分类,再决定路径
|
|
2
|
-
import { writeArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact } from "../core/store.js";
|
|
3
3
|
import { maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
const CATEGORIES = ["实现错误", "根因错误", "测试选择", "环境缺失", "权限被拒", "反复失败"];
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p11-pr-builder.js — 调研 §15:PR 说明忠实反映修改 + Gate 评测
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { requireArtifact, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P11");
|
package/lib/stages/p2-search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p2-search.js — 调研 §4:结构化 Issue → 候选文件 + 证据
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { listRepoFiles, requireArtifact, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf, paramsOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf, paramsOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P2");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p3-code-understanding.js — 调研 §4:调用链理解(基于真实文件内容)
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { requireArtifact, readRepoFile, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf, paramsOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf, paramsOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P3");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p4-hypothesis.js — 调研 §5:每个假设必须可验证
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { requireArtifact, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P4");
|
package/lib/stages/p5-planner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p5-planner.js — 调研 §6:TaskGraph,每节点有契约
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { requireArtifact, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P5");
|
package/lib/stages/p6-coder.js
CHANGED
|
@@ -6,11 +6,11 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { existsSync, readdirSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
9
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
10
10
|
import { requireArtifact, readRepoFile, logEvent } from "./helpers.js";
|
|
11
|
-
import { slugify, timestamp } from "../store.js";
|
|
12
|
-
import { saveRun, sessionPatchesReady } from "../pipeline.js";
|
|
13
|
-
import { sysOf, paramsOf } from "../stageConfig.js";
|
|
11
|
+
import { slugify, timestamp } from "../core/store.js";
|
|
12
|
+
import { saveRun, sessionPatchesReady } from "../core/pipeline.js";
|
|
13
|
+
import { sysOf, paramsOf } from "../core/stageConfig.js";
|
|
14
14
|
|
|
15
15
|
// —— claude CLI 进程管理(stop/删除 Run 时杀进程树,避免孤儿 claude 继续写仓库) ——
|
|
16
16
|
const activeExternals = new Map(); // runDir → ChildProcess
|
package/lib/stages/p7-patch.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { appendArtifactLine } from "../store.js";
|
|
6
|
+
import { appendArtifactLine } from "../core/store.js";
|
|
7
7
|
import { logEvent } from "./helpers.js";
|
|
8
|
+
import { resetRepoClean } from "../infra/repoState.js";
|
|
8
9
|
|
|
9
10
|
// 带stdin输入的异步执行(git apply - 用 stdin 收 diff)
|
|
10
11
|
function execStdin(cmd, args, opts, input) {
|
|
@@ -27,12 +28,15 @@ async function hashRepo(repoDir) {
|
|
|
27
28
|
// patch 清单:优先 coder-report.json(builtin 写 patches 字段;claude 委托写 tasks 字段且
|
|
28
29
|
// patch 路径相对 06-implementation/,这里统一归一化为相对 runDir 全路径);
|
|
29
30
|
// report 缺失时退化为扫描目录(文件名序 = 应用序)。
|
|
30
|
-
|
|
31
|
+
// 导出供 delegateVerify 复用:验证清单 = P7 应用清单,同一口径。
|
|
32
|
+
export function collectPatches(runDir) {
|
|
31
33
|
const normalize = (p) => !p ? null
|
|
32
34
|
: { patch: p.startsWith("06-implementation/") ? p : "06-implementation/" + p.replace(/^\/+/, "") };
|
|
33
35
|
const reportPath = join(runDir, "06-implementation", "coder-report.json");
|
|
34
36
|
if (existsSync(reportPath)) {
|
|
35
|
-
|
|
37
|
+
let report;
|
|
38
|
+
try { report = JSON.parse(readFileSync(reportPath, "utf8")); }
|
|
39
|
+
catch { throw new Error("coder-report.json 损坏(非法 JSON),无法解析补丁清单;请回退 P6 重跑或修复报告文件"); }
|
|
36
40
|
const list = Array.isArray(report.patches) ? report.patches
|
|
37
41
|
: Array.isArray(report.tasks) ? report.tasks.map((t) => ({ patch: t.patch }))
|
|
38
42
|
: [];
|
|
@@ -47,6 +51,11 @@ function collectPatches(runDir) {
|
|
|
47
51
|
export default async function execute(rcx) {
|
|
48
52
|
const patches = collectPatches(rcx.runDir);
|
|
49
53
|
if (!patches.length) throw new Error("P7 无 patch 可应用(06-implementation/patches/ 为空且无 coder-report.json)");
|
|
54
|
+
// A1/A2 修复:应用补丁前先把工作区重置回 HEAD 基线。
|
|
55
|
+
// 打回重跑/回退重跑时上一轮已应用的补丁仍在工作区(直接 re-apply 必失败);
|
|
56
|
+
// worktree 兜底共用基线 repo 的路径下同理清残留。reset 不碰 .gitignore 的目录(如 node_modules)。
|
|
57
|
+
await resetRepoClean(rcx.repoDir);
|
|
58
|
+
logEvent(rcx, { kind: "git", name: "工作区基线重置", detail: "P7 应用补丁前 git reset --hard + git clean -fd" });
|
|
50
59
|
for (const p of patches) {
|
|
51
60
|
const diff = readFileSync(join(rcx.runDir, p.patch), "utf8");
|
|
52
61
|
const hashBefore = await hashRepo(rcx.repoDir);
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { exec } from "node:child_process";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { writeArtifact } from "../store.js";
|
|
6
|
+
import { writeArtifact } from "../core/store.js";
|
|
7
7
|
import { logEvent } from "./helpers.js";
|
|
8
|
-
import { DEFAULT_TEST_TIMEOUT_MS } from "../stageConfig.js";
|
|
8
|
+
import { DEFAULT_TEST_TIMEOUT_MS } from "../core/stageConfig.js";
|
|
9
9
|
|
|
10
10
|
function detectCommand(repoDir) {
|
|
11
11
|
const pkg = join(repoDir, "package.json");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// lib/stages/p9-reviewer.js — 调研 §9:Review 是 PRBuilder 的前置门控
|
|
2
|
-
import { writeArtifact, readArtifact } from "../store.js";
|
|
2
|
+
import { writeArtifact, readArtifact } from "../core/store.js";
|
|
3
3
|
import { requireArtifact, maybeDelegate } from "./helpers.js";
|
|
4
|
-
import { sysOf, paramsOf } from "../stageConfig.js";
|
|
4
|
+
import { sysOf, paramsOf } from "../core/stageConfig.js";
|
|
5
5
|
|
|
6
6
|
export default async function execute(rcx) {
|
|
7
7
|
const delegated = await maybeDelegate(rcx, "P9");
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|