toolflow 3.1.4

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.
Files changed (49) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/README.md +106 -0
  3. package/README_zh.md +109 -0
  4. package/docs/reports/ADVANCED_EVOLUTION_REPORT.md +44 -0
  5. package/docs/reports/AUDIT_AND_OPTIMIZATION_REPORT.md +645 -0
  6. package/docs/reports/COLD_START_REVIEW_EVOLUTION.md +51 -0
  7. package/docs/reports/DEEP_ECOSYSTEM_EVOLUTION.md +43 -0
  8. package/docs/reports/MEMORY.md +18 -0
  9. package/docs/reports/MICHAEL_DISPATCH_RESULT.md +36 -0
  10. package/docs/reports/OPENSOURCE_INTEGRATION_REPORT.md +43 -0
  11. package/docs/reports/PHASE_1_OPTIMIZATION_REPORT.md +87 -0
  12. package/docs/reports/PHASE_2_OPTIMIZATION_REPORT.md +50 -0
  13. package/docs/reports/PHASE_3_OPTIMIZATION_REPORT.md +24 -0
  14. package/docs/reports/PHASE_4_OPTIMIZATION_REPORT.md +28 -0
  15. package/docs/reports/REPORT_TO_MICHAEL.md +101 -0
  16. package/docs/reports/SIGNOFF_AND_RELEASE_REPORT.md +85 -0
  17. package/docs/reports/STAFF_ASSIGNMENTS.md +26 -0
  18. package/docs/reports/TASK_ASSIGNMENTS.md +59 -0
  19. package/docs/reports/V1_6_0_EVOLUTION_REPORT.md +48 -0
  20. package/docs/reports/V1_9_0_HOTFIX_REPORT.md +30 -0
  21. package/docs/reports/V2_0_0_RELEASE_REPORT.md +18 -0
  22. package/docs/reports/V2_2_0_ZERO_SPECIALIZATION_REPORT.md +24 -0
  23. package/docs/reports/V2_3_0_EVOLUTION_REPORT.md +12 -0
  24. package/ecosystem_taxonomy.json +798 -0
  25. package/package.json +46 -0
  26. package/src/blast_radius.ts +302 -0
  27. package/src/deep_ecosystem.ts +523 -0
  28. package/src/degradation_matrix.ts +180 -0
  29. package/src/dehydrator.ts +532 -0
  30. package/src/ecosystem_taxonomy.json +803 -0
  31. package/src/engine.ts +1510 -0
  32. package/src/i18n.ts +89 -0
  33. package/src/index.ts +983 -0
  34. package/src/json_extractor.ts +57 -0
  35. package/src/memory.ts +151 -0
  36. package/src/prompts_manager.ts +262 -0
  37. package/src/review_isolation.ts +188 -0
  38. package/src/state.ts +810 -0
  39. package/src/taxonomy.ts +580 -0
  40. package/src/types.ts +341 -0
  41. package/src/ui.ts +1036 -0
  42. package/src/worker_orchestrator.ts +60 -0
  43. package/tests/challenger_stress_harness.ts +265 -0
  44. package/tests/monorepo_multilang_stress.ts +404 -0
  45. package/tests/sandbox_e2e.ts +167 -0
  46. package/tests/test_json_extractor.ts +44 -0
  47. package/tests/test_modules_1_to_4.ts +106 -0
  48. package/tests/test_suite.ts +1689 -0
  49. package/tsconfig.json +17 -0
@@ -0,0 +1,57 @@
1
+ import fs from "fs";
2
+
3
+ /**
4
+ * 健壮提取大模型输出中的 JSON 对象:
5
+ * 1. 优先提取 Markdown 代码块 (```json ... ```);
6
+ * 2. 次选基于平衡大括号算法 (Balanced Brace Counting) 提取首个最完整外层对象,
7
+ * 精准避开贪婪匹配跨多代码块导致的 SyntaxError 崩溃。
8
+ */
9
+ export function extractValidJsonObject(raw: string): any {
10
+ if (!raw || typeof raw !== "string") {
11
+ throw new Error("Invalid raw text for JSON extraction");
12
+ }
13
+
14
+ // 1. 尝试匹配首个围栏 Markdown json 代码块
15
+ const codeBlockMatch = raw.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/i);
16
+ if (codeBlockMatch) {
17
+ try {
18
+ return JSON.parse(codeBlockMatch[1].trim());
19
+ } catch (_) {}
20
+ }
21
+
22
+ // 2. 基于平衡括号扫描首个外层完整 JSON 对象
23
+ const firstBrace = raw.indexOf("{");
24
+ if (firstBrace === -1) throw new Error("No JSON object found in response");
25
+
26
+ let depth = 0;
27
+ let inString = false;
28
+ let escape = false;
29
+
30
+ for (let i = firstBrace; i < raw.length; i++) {
31
+ const char = raw[i];
32
+ if (escape) {
33
+ escape = false;
34
+ continue;
35
+ }
36
+ if (char === "\\") {
37
+ escape = true;
38
+ continue;
39
+ }
40
+ if (char === '"') {
41
+ inString = !inString;
42
+ continue;
43
+ }
44
+ if (!inString) {
45
+ if (char === "{") depth++;
46
+ else if (char === "}") {
47
+ depth--;
48
+ if (depth === 0) {
49
+ const candidate = raw.slice(firstBrace, i + 1);
50
+ return JSON.parse(candidate);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ throw new Error("Unbalanced braces in LLM response");
57
+ }
package/src/memory.ts ADDED
@@ -0,0 +1,151 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface ArchitecturalLesson {
5
+ id: string;
6
+ topic: string;
7
+ rule: string;
8
+ rationale: string;
9
+ tags: string[];
10
+ timestamp: number;
11
+ }
12
+
13
+ export interface CodebaseMemoryStore {
14
+ version: "1.0.0";
15
+ codebaseId: string;
16
+ updatedAt: number;
17
+ conventions: string[];
18
+ lessons: ArchitecturalLesson[];
19
+ }
20
+
21
+ const MAX_LESSONS = 15;
22
+
23
+ export class CodebaseMemoryManager {
24
+ private memoryFilePath: string;
25
+ private workspaceRoot: string;
26
+
27
+ constructor(workspaceRoot: string = process.cwd()) {
28
+ this.workspaceRoot = workspaceRoot;
29
+ const memoryDir = path.join(workspaceRoot, ".pi", "toolflow", "memory");
30
+ try {
31
+ fs.mkdirSync(memoryDir, { recursive: true });
32
+ } catch (_) {}
33
+ this.memoryFilePath = path.join(memoryDir, "architecture_memory.json");
34
+ }
35
+
36
+ public loadMemory(): CodebaseMemoryStore {
37
+ if (fs.existsSync(this.memoryFilePath)) {
38
+ try {
39
+ const raw = fs.readFileSync(this.memoryFilePath, "utf-8");
40
+ const parsed = JSON.parse(raw) as CodebaseMemoryStore;
41
+ if (parsed && Array.isArray(parsed.lessons)) {
42
+ return parsed;
43
+ }
44
+ } catch (_) {}
45
+ }
46
+ return {
47
+ version: "1.0.0",
48
+ codebaseId: path.basename(this.workspaceRoot),
49
+ updatedAt: Date.now(),
50
+ conventions: [],
51
+ lessons: []
52
+ };
53
+ }
54
+
55
+ private safeWriteStore(store: CodebaseMemoryStore): void {
56
+ const tempPath = `${this.memoryFilePath}.tmp.${process.pid}.${Date.now()}`;
57
+ try {
58
+ const parentDir = path.dirname(this.memoryFilePath);
59
+ if (!fs.existsSync(parentDir)) {
60
+ fs.mkdirSync(parentDir, { recursive: true });
61
+ }
62
+ fs.writeFileSync(tempPath, JSON.stringify(store, null, 2), "utf-8");
63
+
64
+ let renamed = false;
65
+ for (let attempt = 0; attempt < 5; attempt++) {
66
+ try {
67
+ fs.renameSync(tempPath, this.memoryFilePath);
68
+ renamed = true;
69
+ break;
70
+ } catch (_) {
71
+ const start = Date.now();
72
+ while (Date.now() - start < 10) {}
73
+ }
74
+ }
75
+
76
+ if (!renamed) {
77
+ try {
78
+ fs.copyFileSync(tempPath, this.memoryFilePath);
79
+ renamed = true;
80
+ } catch (_) {}
81
+ }
82
+ } catch (_) {
83
+ try {
84
+ fs.writeFileSync(this.memoryFilePath, JSON.stringify(store, null, 2), "utf-8");
85
+ } catch (_) {}
86
+ } finally {
87
+ try {
88
+ if (fs.existsSync(tempPath)) {
89
+ fs.unlinkSync(tempPath);
90
+ }
91
+ } catch (_) {}
92
+ }
93
+ }
94
+
95
+ public recordLesson(topic: string, rule: string, rationale: string, tags: string[] = []): void {
96
+ const store = this.loadMemory();
97
+ const existingIndex = store.lessons.findIndex(l => l.topic === topic || l.rule === rule);
98
+ const newLesson: ArchitecturalLesson = {
99
+ id: "lesson_" + Date.now().toString(36),
100
+ topic,
101
+ rule,
102
+ rationale,
103
+ tags,
104
+ timestamp: Date.now()
105
+ };
106
+
107
+ if (existingIndex >= 0) {
108
+ store.lessons[existingIndex] = newLesson;
109
+ } else {
110
+ store.lessons.push(newLesson);
111
+ }
112
+
113
+ // 滑动窗口控制:严控在最新 MAX_LESSONS (15) 条以内,杜绝 Token 爆炸
114
+ if (store.lessons.length > MAX_LESSONS) {
115
+ store.lessons = store.lessons.slice(-MAX_LESSONS);
116
+ }
117
+
118
+ store.updatedAt = Date.now();
119
+ this.safeWriteStore(store);
120
+ }
121
+
122
+ public recordConvention(convention: string): void {
123
+ const store = this.loadMemory();
124
+ if (!store.conventions.includes(convention)) {
125
+ store.conventions.push(convention);
126
+ if (store.conventions.length > 10) {
127
+ store.conventions = store.conventions.slice(-10);
128
+ }
129
+ store.updatedAt = Date.now();
130
+ this.safeWriteStore(store);
131
+ }
132
+ }
133
+
134
+ public getPromptContextInjection(): string {
135
+ const store = this.loadMemory();
136
+ if (store.lessons.length === 0 && store.conventions.length === 0) {
137
+ return "";
138
+ }
139
+ const lines = ["\n[Codebase 历史架构与避坑记忆 (Memory Directives)]:"];
140
+ store.conventions.forEach(c => lines.push("- 仓库规范: " + c));
141
+ store.lessons.slice(-MAX_LESSONS).forEach(l => {
142
+ const truncatedRationale = l.rationale && l.rationale.length > 100 ? `${l.rationale.slice(0, 97)}...` : l.rationale;
143
+ lines.push("- " + l.topic + ": " + l.rule + " (" + truncatedRationale + ")");
144
+ });
145
+ let result = lines.join("\n");
146
+ if (result.length > 1500) {
147
+ result = result.slice(0, 1490) + "\n...";
148
+ }
149
+ return result;
150
+ }
151
+ }
@@ -0,0 +1,262 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { extractValidJsonObject } from "./json_extractor.js";
5
+
6
+ export interface PromptItemInfo {
7
+ command: string;
8
+ name: string;
9
+ description: string;
10
+ scope: "global" | "project" | "package";
11
+ category: "user" | "system"; // 分组:用户自定义(global/project) vs 系统内置(package)
12
+ filePath: string;
13
+ updatedAt: number; // 文件修改时间戳,用于将最新添加/修改的排在顶端
14
+ lastUsedAt?: number; // 最近使用时间戳(MRU)
15
+ }
16
+
17
+ export class PromptsManager {
18
+ public static scanAllPrompts(cwd: string = process.cwd()): PromptItemInfo[] {
19
+ const results: PromptItemInfo[] = [];
20
+ const seen = new Set<string>();
21
+
22
+ const checkDir = (dir: string, scope: "global" | "project" | "package") => {
23
+ if (!fs.existsSync(dir)) return;
24
+ try {
25
+ const files = fs.readdirSync(dir);
26
+ for (const file of files) {
27
+ if (file.endsWith(".md")) {
28
+ const name = file.replace(/.md$/, "");
29
+ const command = "/" + name;
30
+ if (seen.has(command)) continue;
31
+ seen.add(command);
32
+
33
+ const filePath = path.join(dir, file);
34
+ let description = "";
35
+ try {
36
+ const content = fs.readFileSync(filePath, "utf8");
37
+ const parts = content.split("description:");
38
+ if (parts.length > 1) {
39
+ description = parts[1].split("\n")[0].trim();
40
+ }
41
+ } catch (_) {}
42
+
43
+ let updatedAt = 0;
44
+ try {
45
+ updatedAt = fs.statSync(filePath).mtimeMs;
46
+ } catch (_) {}
47
+
48
+ const category: "user" | "system" = scope === "package" ? "system" : "user";
49
+
50
+ results.push({
51
+ command,
52
+ name,
53
+ description: description || "自定义提示词模板 [" + command + "]",
54
+ scope,
55
+ category,
56
+ filePath,
57
+ updatedAt
58
+ });
59
+ }
60
+ }
61
+ } catch (_) {}
62
+ };
63
+
64
+ const projectDir = path.join(cwd, ".pi", "prompts");
65
+ checkDir(projectDir, "project");
66
+
67
+ const piAgentBase = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
68
+ const globalDir = path.join(piAgentBase, "prompts");
69
+ checkDir(globalDir, "global");
70
+
71
+ const pkgDir = path.join(piAgentBase, "npm", "node_modules");
72
+ if (fs.existsSync(pkgDir)) {
73
+ try {
74
+ const scanPkgs = (base: string, depth = 0) => {
75
+ if (depth > 2) return;
76
+ const items = fs.readdirSync(base);
77
+ for (const item of items) {
78
+ const full = path.join(base, item);
79
+ if (item === "prompts") {
80
+ checkDir(full, "package");
81
+ } else if (fs.statSync(full).isDirectory() && !item.startsWith(".")) {
82
+ scanPkgs(full, depth + 1);
83
+ }
84
+ }
85
+ };
86
+ scanPkgs(pkgDir);
87
+ } catch (_) {}
88
+ }
89
+
90
+ // 读取最近使用时间戳(MRU)
91
+ const mruMap = this.loadMruMap();
92
+ for (const item of results) {
93
+ if (mruMap[item.command]) {
94
+ item.lastUsedAt = mruMap[item.command];
95
+ }
96
+ }
97
+
98
+ // 默认按照最后修改时间排序
99
+ results.sort((a, b) => b.updatedAt - a.updatedAt);
100
+ return results;
101
+ }
102
+
103
+ private static getMruFilePath(): string {
104
+ const piAgentBase = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
105
+ return path.join(piAgentBase, "toolflow_prompts_mru.json");
106
+ }
107
+
108
+ private static loadMruMap(): Record<string, number> {
109
+ const file = this.getMruFilePath();
110
+ if (!fs.existsSync(file)) return {};
111
+ try {
112
+ return JSON.parse(fs.readFileSync(file, "utf8"));
113
+ } catch (_) {
114
+ return {};
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 记录提示词被调用/选中,更新 MRU 时间戳
120
+ */
121
+ public static recordPromptUsage(command: string): void {
122
+ const mruMap = this.loadMruMap();
123
+ mruMap[command] = Date.now();
124
+ try {
125
+ const file = this.getMruFilePath();
126
+ fs.mkdirSync(path.dirname(file), { recursive: true });
127
+ fs.writeFileSync(file, JSON.stringify(mruMap, null, 2), "utf8");
128
+ } catch (_) {}
129
+ }
130
+
131
+ /**
132
+ * 获取最近使用的提示词列表(Top N),若无使用记录则回退为按最新添加时间排序
133
+ */
134
+ public static getRecentPrompts(prompts: PromptItemInfo[], limit: number = 5): PromptItemInfo[] {
135
+ const mruMap = this.loadMruMap();
136
+ const withUsage = prompts.filter(p => mruMap[p.command] !== undefined);
137
+ if (withUsage.length > 0) {
138
+ withUsage.sort((a, b) => (mruMap[b.command] || 0) - (mruMap[a.command] || 0));
139
+ return withUsage.slice(0, limit);
140
+ }
141
+ // 回退展示按更新时间排序的前 N 个
142
+ return [...prompts].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
143
+ }
144
+
145
+ /**
146
+ * 提取指定提示词文件的正文前 N 行用于即时预览(自动剥离 frontmatter 头部)
147
+ */
148
+ public static getPromptPreviewLines(item: PromptItemInfo, maxLines: number = 3): string[] {
149
+ const body = this.getPromptContent(item);
150
+ if (!body) return [];
151
+ return body
152
+ .split(/\r?\n/)
153
+ .map(l => l.trim())
154
+ .filter(l => l.length > 0)
155
+ .slice(0, maxLines);
156
+ }
157
+
158
+ public static createPrompt(name: string, description: string, content: string, scope: "global" | "project" = "global", cwd: string = process.cwd()): PromptItemInfo {
159
+ const cleanName = name.replace(/^\/+/, "").trim();
160
+ const piAgentBase = process.env.PI_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
161
+ const targetDir = scope === "project"
162
+ ? path.join(cwd, ".pi", "prompts")
163
+ : path.join(piAgentBase, "prompts");
164
+
165
+ if (!fs.existsSync(targetDir)) {
166
+ fs.mkdirSync(targetDir, { recursive: true });
167
+ }
168
+
169
+ const filePath = path.join(targetDir, cleanName + ".md");
170
+ const fileBody = [
171
+ "---",
172
+ "description: " + description.trim(),
173
+ "---",
174
+ "",
175
+ content.trim(),
176
+ ""
177
+ ].join("\n");
178
+
179
+ fs.writeFileSync(filePath, fileBody, "utf8");
180
+
181
+ return {
182
+ command: "/" + cleanName,
183
+ name: cleanName,
184
+ description: description.trim(),
185
+ scope,
186
+ category: "user",
187
+ filePath,
188
+ updatedAt: Date.now()
189
+ };
190
+ }
191
+
192
+ public static getPromptContent(item: PromptItemInfo): string {
193
+ if (!item.filePath || !fs.existsSync(item.filePath)) {
194
+ return "";
195
+ }
196
+ try {
197
+ const raw = fs.readFileSync(item.filePath, "utf8");
198
+ if (raw.startsWith("---")) {
199
+ const parts = raw.split("---");
200
+ if (parts.length >= 3) {
201
+ return parts.slice(2).join("---").trim();
202
+ }
203
+ }
204
+ return raw.trim();
205
+ } catch (_) {
206
+ return "";
207
+ }
208
+ }
209
+
210
+ public static deletePrompt(item: PromptItemInfo): boolean {
211
+ if (!item.filePath || !fs.existsSync(item.filePath)) {
212
+ return false;
213
+ }
214
+ try {
215
+ fs.unlinkSync(item.filePath);
216
+ return true;
217
+ } catch (_) {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ public static async autoSummarizeTagWithLLM(content: string, ctx?: any): Promise<{ name: string; description: string }> {
223
+ const fallbackName = "my-prompt";
224
+ const fallbackDesc = content.slice(0, 30).replace(/\r?\n/g, " ");
225
+
226
+ if (!ctx || !(ctx as any).modelRegistry || !(ctx as any).model) {
227
+ return { name: fallbackName, description: fallbackDesc };
228
+ }
229
+
230
+ try {
231
+ const mr = (ctx as any).modelRegistry;
232
+ const model = (ctx as any).model;
233
+ const prompt = `请为以下这段用于 AI 编程的 Prompt 模版生成一个极简短的命令名称和一个精练的中文说明。
234
+
235
+ [PROMPT 内容]:
236
+ ${content.slice(0, 1500)}
237
+
238
+ 严格按 JSON 输出:
239
+ {
240
+ "name": "极简英文命令名(如 wechat-test, code-review, rust-api 等,纯小写字母与连字符)",
241
+ "description": "一句话大白话精炼说明(20字以内,讲清楚它的作用)"
242
+ }`;
243
+
244
+ const resp = await mr.complete(model, {
245
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
246
+ options: { temperature: 0.2 }
247
+ });
248
+
249
+ const raw = resp.content?.[0]?.type === "text" ? resp.content[0].text : "";
250
+ // 稳健提取 JSON 块,剥离 Markdown 围栏并支持平衡括号防崩溃
251
+ const parsed = extractValidJsonObject(raw);
252
+
253
+ return {
254
+ name: (parsed.name || fallbackName).replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase(),
255
+ description: parsed.description || fallbackDesc
256
+ };
257
+ } catch (_) {
258
+ return { name: fallbackName, description: fallbackDesc };
259
+ }
260
+ }
261
+
262
+ }
@@ -0,0 +1,188 @@
1
+ import { execSync } from "child_process";
2
+ import * as path from "path";
3
+ import { BlueprintStage } from "./types.js";
4
+
5
+ export interface ReviewSnapshot {
6
+ baseSha?: string;
7
+ headSha?: string;
8
+ diffSummary: string;
9
+ detailedDiff: string;
10
+ changedFiles: string[];
11
+ hasChanges?: boolean;
12
+ }
13
+
14
+ export interface ReviewIsolationResult {
15
+ isIsolated: boolean;
16
+ isolationMode: "fresh_subagent" | "context_stripped_turn";
17
+ snapshot: ReviewSnapshot;
18
+ isolatedSystemPrompt: string;
19
+ isolatedUserPrompt: string;
20
+ }
21
+
22
+ /**
23
+ * 捕获当前 Git 仓库的物理变更 Diff 快照
24
+ */
25
+ export function captureReviewDiffSnapshot(cwd: string = process.cwd()): ReviewSnapshot {
26
+ try {
27
+ // 检查是否在 git 仓库中
28
+ const isGit = execSync("git rev-parse --is-inside-work-tree", {
29
+ cwd,
30
+ encoding: "utf-8",
31
+ stdio: ["pipe", "pipe", "ignore"],
32
+ timeout: 2000
33
+ }).trim() === "true";
34
+
35
+ if (!isGit) {
36
+ return {
37
+ diffSummary: "Non-git directory: unable to extract git diff.",
38
+ detailedDiff: "",
39
+ changedFiles: []
40
+ };
41
+ }
42
+
43
+ let headSha = "";
44
+ try {
45
+ headSha = execSync("git rev-parse HEAD", {
46
+ cwd,
47
+ encoding: "utf-8",
48
+ stdio: ["pipe", "pipe", "ignore"],
49
+ timeout: 2000
50
+ }).trim();
51
+ } catch {
52
+ // 新仓库可能尚无 HEAD
53
+ headSha = "INITIAL_UNCOMMITTED";
54
+ }
55
+
56
+ // 获取变更文件清单 (staged + unstaged + untracked)
57
+ const statusOut = execSync("git status --porcelain", {
58
+ cwd,
59
+ encoding: "utf-8",
60
+ stdio: ["pipe", "pipe", "ignore"],
61
+ timeout: 3000
62
+ }).trim();
63
+
64
+ const changedFiles: string[] = [];
65
+ if (statusOut) {
66
+ const lines = statusOut.split("\n");
67
+ for (const line of lines) {
68
+ const filePart = line.substring(3).trim();
69
+ if (filePart) changedFiles.push(filePart);
70
+ }
71
+ }
72
+
73
+ // 获取详细 diff
74
+ let detailedDiff = "";
75
+ try {
76
+ detailedDiff = execSync("git diff HEAD", {
77
+ cwd,
78
+ encoding: "utf-8",
79
+ stdio: ["pipe", "pipe", "ignore"],
80
+ timeout: 5000
81
+ });
82
+ if (!detailedDiff.trim()) {
83
+ // 如果没有与 HEAD 的 diff,尝试抓取未暂存 diff
84
+ detailedDiff = execSync("git diff", {
85
+ cwd,
86
+ encoding: "utf-8",
87
+ stdio: ["pipe", "pipe", "ignore"],
88
+ timeout: 5000
89
+ });
90
+ }
91
+ } catch {
92
+ detailedDiff = statusOut;
93
+ }
94
+
95
+ // 限制 diff 尺寸以防膨胀
96
+ if (detailedDiff.length > 8000) {
97
+ detailedDiff = detailedDiff.substring(0, 8000) + "\n... [Diff truncated to 8000 chars for context hygiene]";
98
+ }
99
+
100
+ const summary = `${changedFiles.length} file(s) changed: ${changedFiles.slice(0, 5).join(", ")}${changedFiles.length > 5 ? "..." : ""}`;
101
+
102
+ return {
103
+ headSha,
104
+ diffSummary: summary,
105
+ detailedDiff,
106
+ changedFiles,
107
+ hasChanges: changedFiles.length > 0
108
+ };
109
+ } catch (err: any) {
110
+ return {
111
+ diffSummary: `Failed to inspect git diff: ${err?.message || String(err)}`,
112
+ detailedDiff: "",
113
+ changedFiles: [],
114
+ hasChanges: false
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 构建针对 Reviewer 智能体的冷启动隔离上下文与脱水提示词契约。
121
+ * 杜绝承袭 Implementer 的历史推理心智,以客观物理视角审视 Diff 与验收标准。
122
+ */
123
+ export function buildColdStartReviewContract(
124
+ stage: BlueprintStage,
125
+ stageIndex: number,
126
+ totalStages: number,
127
+ snapshot: ReviewSnapshot
128
+ ): ReviewIsolationResult {
129
+ const isolatedSystemPrompt = [
130
+ "=== ZERO-MEMORY INDEPENDENT CODE AUDITOR ===",
131
+ "ROLE: You are an independent, objective software auditor and QA reviewer.",
132
+ "ISOLATION STATUS: Cold start enabled. You have NO prior implementation context or bias.",
133
+ "OBJECTIVE: Objectively verify the git changes against the expected artifacts and quality gates.",
134
+ "GUIDELINES:",
135
+ "1. Base your verdict strictly on actual physical file diffs and command verifications, NOT developer promises.",
136
+ "2. Check for syntax correctness, edge case security, blast radius violations, and test assertions.",
137
+ "3. Be adversarial yet fair. Report concrete bugs or approve if all quality gates pass.",
138
+ "ALLOWED OPERATIONS: Read, inspect files, execute verification commands (bash/powershell)."
139
+ ].join("\n");
140
+
141
+ const gateCommands = stage.verificationCommands && stage.verificationCommands.length > 0
142
+ ? `\nVerification Gate: ${stage.verificationCommands.join(" && ")}`
143
+ : "";
144
+
145
+ const isolatedUserPrompt = [
146
+ `[Stage ${stageIndex + 1}/${totalStages}: ${stage.title} (Independent Review)]`,
147
+ `Review Target: ${stage.expectedArtifact}`,
148
+ `Core Objective: ${stage.coreObjective}${gateCommands}`,
149
+ `Changes Under Review:\n${snapshot.diffSummary}`,
150
+ snapshot.detailedDiff ? `\n--- DIFF AUDIT PAYLOAD ---\n${snapshot.detailedDiff}\n--- END DIFF ---` : "",
151
+ `\nInstruction: Perform cold-start audit on these changes. Validate correctness and artifact integrity.`
152
+ ].join("\n");
153
+
154
+ return {
155
+ isIsolated: true,
156
+ isolationMode: "fresh_subagent",
157
+ snapshot,
158
+ isolatedSystemPrompt,
159
+ isolatedUserPrompt
160
+ };
161
+ }
162
+
163
+ /**
164
+ * 审查阶段运行时工具防线 (Review Isolation Guard)
165
+ * 物理阻断任何修改代码的动作 (write, edit 等),强制要求审核员保持“只读/只验证”客观状态。
166
+ */
167
+ export class ReviewIsolationGuard {
168
+ private active: boolean = false;
169
+ private allowedAuditTools = new Set(["read", "bash", "powershell", "grep", "find", "goal_complete", "goal_blocked", "goal_wait", "mcp"]);
170
+
171
+ public activate(): void {
172
+ this.active = true;
173
+ }
174
+
175
+ public deactivate(): void {
176
+ this.active = false;
177
+ }
178
+
179
+ public isActive(): boolean {
180
+ return this.active;
181
+ }
182
+
183
+ public isToolAllowedInReview(toolName: string): boolean {
184
+ if (!this.active) return true;
185
+ return this.allowedAuditTools.has(toolName);
186
+ }
187
+ }
188
+