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.
- package/.github/workflows/ci.yml +31 -0
- package/README.md +106 -0
- package/README_zh.md +109 -0
- package/docs/reports/ADVANCED_EVOLUTION_REPORT.md +44 -0
- package/docs/reports/AUDIT_AND_OPTIMIZATION_REPORT.md +645 -0
- package/docs/reports/COLD_START_REVIEW_EVOLUTION.md +51 -0
- package/docs/reports/DEEP_ECOSYSTEM_EVOLUTION.md +43 -0
- package/docs/reports/MEMORY.md +18 -0
- package/docs/reports/MICHAEL_DISPATCH_RESULT.md +36 -0
- package/docs/reports/OPENSOURCE_INTEGRATION_REPORT.md +43 -0
- package/docs/reports/PHASE_1_OPTIMIZATION_REPORT.md +87 -0
- package/docs/reports/PHASE_2_OPTIMIZATION_REPORT.md +50 -0
- package/docs/reports/PHASE_3_OPTIMIZATION_REPORT.md +24 -0
- package/docs/reports/PHASE_4_OPTIMIZATION_REPORT.md +28 -0
- package/docs/reports/REPORT_TO_MICHAEL.md +101 -0
- package/docs/reports/SIGNOFF_AND_RELEASE_REPORT.md +85 -0
- package/docs/reports/STAFF_ASSIGNMENTS.md +26 -0
- package/docs/reports/TASK_ASSIGNMENTS.md +59 -0
- package/docs/reports/V1_6_0_EVOLUTION_REPORT.md +48 -0
- package/docs/reports/V1_9_0_HOTFIX_REPORT.md +30 -0
- package/docs/reports/V2_0_0_RELEASE_REPORT.md +18 -0
- package/docs/reports/V2_2_0_ZERO_SPECIALIZATION_REPORT.md +24 -0
- package/docs/reports/V2_3_0_EVOLUTION_REPORT.md +12 -0
- package/ecosystem_taxonomy.json +798 -0
- package/package.json +46 -0
- package/src/blast_radius.ts +302 -0
- package/src/deep_ecosystem.ts +523 -0
- package/src/degradation_matrix.ts +180 -0
- package/src/dehydrator.ts +532 -0
- package/src/ecosystem_taxonomy.json +803 -0
- package/src/engine.ts +1510 -0
- package/src/i18n.ts +89 -0
- package/src/index.ts +983 -0
- package/src/json_extractor.ts +57 -0
- package/src/memory.ts +151 -0
- package/src/prompts_manager.ts +262 -0
- package/src/review_isolation.ts +188 -0
- package/src/state.ts +810 -0
- package/src/taxonomy.ts +580 -0
- package/src/types.ts +341 -0
- package/src/ui.ts +1036 -0
- package/src/worker_orchestrator.ts +60 -0
- package/tests/challenger_stress_harness.ts +265 -0
- package/tests/monorepo_multilang_stress.ts +404 -0
- package/tests/sandbox_e2e.ts +167 -0
- package/tests/test_json_extractor.ts +44 -0
- package/tests/test_modules_1_to_4.ts +106 -0
- package/tests/test_suite.ts +1689 -0
- package/tsconfig.json +17 -0
package/src/state.ts
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import type {
|
|
6
|
+
SessionPlanState,
|
|
7
|
+
Blueprint,
|
|
8
|
+
ArtifactRecord,
|
|
9
|
+
BlueprintStage,
|
|
10
|
+
StageVerificationResult,
|
|
11
|
+
StageSnapshot
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
const state: SessionPlanState = {
|
|
15
|
+
currentBlueprint: null,
|
|
16
|
+
currentStageIndex: 0,
|
|
17
|
+
stepByStepGate: true,
|
|
18
|
+
status: "idle",
|
|
19
|
+
artifactLedger: {},
|
|
20
|
+
snapshots: {},
|
|
21
|
+
dynamicTargetFiles: [],
|
|
22
|
+
retryCount: 0
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getPersistFilePath(cwd: string = process.cwd()): string {
|
|
26
|
+
const dotPiDir = path.join(cwd, ".pi");
|
|
27
|
+
if (!fs.existsSync(dotPiDir)) {
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(dotPiDir, { recursive: true });
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
}
|
|
32
|
+
return path.join(dotPiDir, "blueprint_state.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function atomicWriteFileSync(filePath: string, content: string): boolean {
|
|
36
|
+
const tempPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
37
|
+
let success = false;
|
|
38
|
+
try {
|
|
39
|
+
const parentDir = path.dirname(filePath);
|
|
40
|
+
if (!fs.existsSync(parentDir)) {
|
|
41
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
fs.writeFileSync(tempPath, content, "utf-8");
|
|
44
|
+
|
|
45
|
+
let renamed = false;
|
|
46
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
fs.renameSync(tempPath, filePath);
|
|
49
|
+
renamed = true;
|
|
50
|
+
break;
|
|
51
|
+
} catch (_) {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
while (Date.now() - start < 10) {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!renamed) {
|
|
58
|
+
try {
|
|
59
|
+
fs.copyFileSync(tempPath, filePath);
|
|
60
|
+
renamed = true;
|
|
61
|
+
} catch (_) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (renamed) {
|
|
65
|
+
try {
|
|
66
|
+
fs.copyFileSync(filePath, `${filePath}.bak`);
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
success = true;
|
|
69
|
+
}
|
|
70
|
+
} catch (_) {
|
|
71
|
+
success = false;
|
|
72
|
+
} finally {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(tempPath)) {
|
|
75
|
+
fs.unlinkSync(tempPath);
|
|
76
|
+
}
|
|
77
|
+
} catch (_) {}
|
|
78
|
+
}
|
|
79
|
+
return success;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function saveSessionStateToFile(cwd: string = process.cwd()): boolean {
|
|
83
|
+
try {
|
|
84
|
+
const filePath = getPersistFilePath(cwd);
|
|
85
|
+
return atomicWriteFileSync(filePath, JSON.stringify(state, null, 2));
|
|
86
|
+
} catch (_) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function loadPersistedSessionState(cwd: string = process.cwd()): SessionPlanState | null {
|
|
92
|
+
const filePath = getPersistFilePath(cwd);
|
|
93
|
+
let content: string | null = null;
|
|
94
|
+
if (fs.existsSync(filePath)) {
|
|
95
|
+
try {
|
|
96
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
}
|
|
99
|
+
if (!content && fs.existsSync(`${filePath}.bak`)) {
|
|
100
|
+
try {
|
|
101
|
+
content = fs.readFileSync(`${filePath}.bak`, "utf-8");
|
|
102
|
+
} catch (_) {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (content) {
|
|
106
|
+
try {
|
|
107
|
+
const loaded = JSON.parse(content) as SessionPlanState;
|
|
108
|
+
if (loaded && loaded.currentBlueprint) {
|
|
109
|
+
state.currentBlueprint = loaded.currentBlueprint;
|
|
110
|
+
state.currentStageIndex = loaded.currentStageIndex ?? 0;
|
|
111
|
+
state.stepByStepGate = loaded.stepByStepGate ?? true;
|
|
112
|
+
state.status = loaded.status ?? "idle";
|
|
113
|
+
state.artifactLedger = loaded.artifactLedger ?? {};
|
|
114
|
+
state.snapshots = loaded.snapshots ?? {};
|
|
115
|
+
state.dynamicTargetFiles = loaded.dynamicTargetFiles ?? [];
|
|
116
|
+
state.retryCount = loaded.retryCount ?? 0;
|
|
117
|
+
state.shadowCommitHash = loaded.shadowCommitHash;
|
|
118
|
+
return { ...state };
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {
|
|
121
|
+
if (fs.existsSync(`${filePath}.bak`)) {
|
|
122
|
+
try {
|
|
123
|
+
const bakContent = fs.readFileSync(`${filePath}.bak`, "utf-8");
|
|
124
|
+
const loaded = JSON.parse(bakContent) as SessionPlanState;
|
|
125
|
+
if (loaded && loaded.currentBlueprint) {
|
|
126
|
+
state.currentBlueprint = loaded.currentBlueprint;
|
|
127
|
+
state.currentStageIndex = loaded.currentStageIndex ?? 0;
|
|
128
|
+
state.stepByStepGate = loaded.stepByStepGate ?? true;
|
|
129
|
+
state.status = loaded.status ?? "idle";
|
|
130
|
+
state.artifactLedger = loaded.artifactLedger ?? {};
|
|
131
|
+
state.snapshots = loaded.snapshots ?? {};
|
|
132
|
+
state.dynamicTargetFiles = loaded.dynamicTargetFiles ?? [];
|
|
133
|
+
state.retryCount = loaded.retryCount ?? 0;
|
|
134
|
+
state.shadowCommitHash = loaded.shadowCommitHash;
|
|
135
|
+
return { ...state };
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function clearMemoryState(): void {
|
|
145
|
+
state.currentBlueprint = null;
|
|
146
|
+
state.currentStageIndex = 0;
|
|
147
|
+
state.stepByStepGate = true;
|
|
148
|
+
state.status = "idle";
|
|
149
|
+
state.artifactLedger = {};
|
|
150
|
+
state.snapshots = {};
|
|
151
|
+
state.dynamicTargetFiles = [];
|
|
152
|
+
state.changedFiles = [];
|
|
153
|
+
state.retryCount = 0;
|
|
154
|
+
state.shadowCommitHash = undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function resetState(cwd: string = process.cwd()): void {
|
|
158
|
+
clearMemoryState();
|
|
159
|
+
const gitInfo = getGitChangedFiles(cwd);
|
|
160
|
+
state.changedFiles = [...gitInfo.changedFiles, ...gitInfo.untrackedFiles];
|
|
161
|
+
try {
|
|
162
|
+
const filePath = getPersistFilePath(cwd);
|
|
163
|
+
if (fs.existsSync(filePath)) {
|
|
164
|
+
fs.unlinkSync(filePath);
|
|
165
|
+
}
|
|
166
|
+
const bakPath = `${filePath}.bak`;
|
|
167
|
+
if (fs.existsSync(bakPath)) {
|
|
168
|
+
fs.unlinkSync(bakPath);
|
|
169
|
+
}
|
|
170
|
+
// 清理残留的原子写入临时文件 (如 blueprint_state.json.tmp.*)
|
|
171
|
+
const parentDir = path.dirname(filePath);
|
|
172
|
+
if (fs.existsSync(parentDir)) {
|
|
173
|
+
const entries = fs.readdirSync(parentDir);
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (entry.startsWith("blueprint_state.json.tmp.")) {
|
|
176
|
+
try {
|
|
177
|
+
fs.unlinkSync(path.join(parentDir, entry));
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (_) {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getSessionState(): Readonly<SessionPlanState> {
|
|
186
|
+
return { ...state };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 获取 Git 状态与变更文件列表 (Git-aware tracking)
|
|
191
|
+
*/
|
|
192
|
+
export function getGitChangedFiles(cwd: string = process.cwd()): { isGit: boolean; changedFiles: string[]; untrackedFiles: string[] } {
|
|
193
|
+
try {
|
|
194
|
+
// 使用 rev-parse 探测是否在 git 工作树内,完美支持子目录与 git worktree
|
|
195
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 2000 });
|
|
196
|
+
const statusOutput = execSync("git status --porcelain", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 3000 });
|
|
197
|
+
const changedFiles: string[] = [];
|
|
198
|
+
const untrackedFiles: string[] = [];
|
|
199
|
+
for (const line of statusOutput.split(/\r?\n/)) {
|
|
200
|
+
if (!line.trim()) continue;
|
|
201
|
+
const code = line.slice(0, 2);
|
|
202
|
+
let file = line.slice(3).trim();
|
|
203
|
+
if (file.startsWith('"') && file.endsWith('"')) {
|
|
204
|
+
file = file.slice(1, -1);
|
|
205
|
+
}
|
|
206
|
+
file = file.replace(/\\/g, "/");
|
|
207
|
+
if (code.includes("?")) {
|
|
208
|
+
untrackedFiles.push(file);
|
|
209
|
+
} else {
|
|
210
|
+
changedFiles.push(file);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { isGit: true, changedFiles, untrackedFiles };
|
|
214
|
+
} catch (_) {
|
|
215
|
+
return { isGit: false, changedFiles: [], untrackedFiles: [] };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 递归收集目录下的重要候选文件(防止扫描 node_modules / .git / dist 等大目录)
|
|
221
|
+
*/
|
|
222
|
+
function scanCandidateFiles(dir: string, baseDir: string = dir, maxFiles: number = 50): string[] {
|
|
223
|
+
const ignoreDirs = new Set(["node_modules", ".git", "dist", "build", "target", ".cache", ".pi", "coverage", ".next", ".turbo", "venv", ".venv"]);
|
|
224
|
+
const results: string[] = [];
|
|
225
|
+
|
|
226
|
+
function walk(current: string) {
|
|
227
|
+
if (results.length >= maxFiles) return;
|
|
228
|
+
try {
|
|
229
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (results.length >= maxFiles) break;
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
if (!ignoreDirs.has(entry.name) && !entry.name.startsWith(".")) {
|
|
234
|
+
walk(path.join(current, entry.name));
|
|
235
|
+
}
|
|
236
|
+
} else if (entry.isFile()) {
|
|
237
|
+
const rel = path.relative(baseDir, path.join(current, entry.name)).replace(/\\/g, "/");
|
|
238
|
+
results.push(rel);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} catch (_) {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
walk(dir);
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 自动在 Stage 开始前创建静默安全快照点 (支持文件级快照 + Git 影子引用)
|
|
250
|
+
*/
|
|
251
|
+
export function createStageSnapshot(stageIndex: number, cwd: string = process.cwd()): StageSnapshot | null {
|
|
252
|
+
if (!state.currentBlueprint) {
|
|
253
|
+
console.error("[CRITICAL-SNAP-ERR] state.currentBlueprint 为空!");
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const stage = state.currentBlueprint.stages[stageIndex];
|
|
257
|
+
if (!stage) {
|
|
258
|
+
console.error(`[CRITICAL-SNAP-ERR] stageIndex ${stageIndex} 超出范围 (长度 ${state.currentBlueprint.stages.length})`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const fileHashes: Record<string, string> = {};
|
|
263
|
+
const fileContents: Record<string, string> = {};
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const gitInfo = getGitChangedFiles(cwd);
|
|
267
|
+
const candidateFiles = new Set<string>();
|
|
268
|
+
|
|
269
|
+
// 1. 阶段预期产物
|
|
270
|
+
if (stage.expectedArtifact) candidateFiles.add(stage.expectedArtifact);
|
|
271
|
+
if (stage.expectedArtifacts) {
|
|
272
|
+
for (const f of stage.expectedArtifacts) candidateFiles.add(f);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. Git 识别出的已变更文件
|
|
276
|
+
for (const f of gitInfo.changedFiles) candidateFiles.add(f);
|
|
277
|
+
for (const f of gitInfo.untrackedFiles) candidateFiles.add(f);
|
|
278
|
+
|
|
279
|
+
// 3. 动态发现或工作区已有重要文件
|
|
280
|
+
const scanned = scanCandidateFiles(cwd, cwd, 30);
|
|
281
|
+
for (const f of scanned) candidateFiles.add(f);
|
|
282
|
+
|
|
283
|
+
for (const rel of candidateFiles) {
|
|
284
|
+
const full = path.isAbsolute(rel) ? rel : path.resolve(cwd, rel);
|
|
285
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
286
|
+
try {
|
|
287
|
+
const stats = fs.statSync(full);
|
|
288
|
+
if (stats.size < 2 * 1024 * 1024) {
|
|
289
|
+
const content = fs.readFileSync(full, "utf-8");
|
|
290
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
291
|
+
fileHashes[rel] = hash;
|
|
292
|
+
fileContents[rel] = content;
|
|
293
|
+
}
|
|
294
|
+
} catch (_) {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let shadowRef: string | undefined;
|
|
299
|
+
if (gitInfo.isGit) {
|
|
300
|
+
try {
|
|
301
|
+
const headSha = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"], timeout: 2000 }).trim();
|
|
302
|
+
shadowRef = headSha;
|
|
303
|
+
state.shadowCommitHash = headSha;
|
|
304
|
+
} catch (_) {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const snapshot: StageSnapshot = {
|
|
308
|
+
stageIndex,
|
|
309
|
+
stageId: stage.stageId,
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
fileHashes,
|
|
312
|
+
fileContents,
|
|
313
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
314
|
+
gitTracked: gitInfo.isGit,
|
|
315
|
+
shadowRef
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (!state.snapshots) state.snapshots = {};
|
|
319
|
+
state.snapshots[stageIndex] = snapshot;
|
|
320
|
+
state.retryCount = 0; // 重置本阶段自愈重试计数
|
|
321
|
+
saveSessionStateToFile(cwd);
|
|
322
|
+
return snapshot;
|
|
323
|
+
} catch (snapErr: any) {
|
|
324
|
+
console.error("[CRITICAL-SNAP-ERR]", snapErr?.message || snapErr);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 一键秒级回滚至阶段开始前的纯净状态 (One-Click Stage Rollback)
|
|
331
|
+
*/
|
|
332
|
+
export function rollbackStage(stageIndex: number = state.currentStageIndex, cwd: string = process.cwd()): { success: boolean; message: string; mentalResetPrompt?: string } {
|
|
333
|
+
if (!state.currentBlueprint) {
|
|
334
|
+
return { success: false, message: "当前尚未激活任何蓝图,无法执行回滚。" };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const snapshot = state.snapshots?.[stageIndex];
|
|
338
|
+
const stage = state.currentBlueprint.stages[stageIndex];
|
|
339
|
+
const stageName = stage?.title || `Stage ${stageIndex + 1}`;
|
|
340
|
+
|
|
341
|
+
if (!snapshot) {
|
|
342
|
+
if (stage?.expectedArtifact && state.artifactLedger[stage.expectedArtifact]) {
|
|
343
|
+
delete state.artifactLedger[stage.expectedArtifact];
|
|
344
|
+
}
|
|
345
|
+
state.retryCount = 0;
|
|
346
|
+
saveSessionStateToFile(cwd);
|
|
347
|
+
return { success: true, message: `已重置「${stageName}」的执行状态机,可重新开始执行本阶段。` };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
if (snapshot.fileContents) {
|
|
352
|
+
for (const [relPath, content] of Object.entries(snapshot.fileContents)) {
|
|
353
|
+
const full = path.isAbsolute(relPath) ? relPath : path.resolve(cwd, relPath);
|
|
354
|
+
const parent = path.dirname(full);
|
|
355
|
+
if (!fs.existsSync(parent)) {
|
|
356
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
fs.writeFileSync(full, content, "utf-8");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 检查并清理快照创建后新生成的孤儿文件
|
|
363
|
+
const currentGit = getGitChangedFiles(cwd);
|
|
364
|
+
const candidateFiles = new Set([...currentGit.changedFiles, ...currentGit.untrackedFiles]);
|
|
365
|
+
if (stage?.expectedArtifact) candidateFiles.add(stage.expectedArtifact);
|
|
366
|
+
if (stage?.expectedArtifacts) stage.expectedArtifacts.forEach(f => candidateFiles.add(f));
|
|
367
|
+
|
|
368
|
+
for (const rel of candidateFiles) {
|
|
369
|
+
const full = path.isAbsolute(rel) ? rel : path.resolve(cwd, rel);
|
|
370
|
+
if (!snapshot.fileHashes?.[rel] && fs.existsSync(full)) {
|
|
371
|
+
try {
|
|
372
|
+
if (fs.statSync(full).isFile()) {
|
|
373
|
+
fs.unlinkSync(full);
|
|
374
|
+
}
|
|
375
|
+
} catch (_) {}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 移除本阶段产物的验收记录
|
|
380
|
+
if (stage?.expectedArtifact && state.artifactLedger[stage.expectedArtifact]) {
|
|
381
|
+
delete state.artifactLedger[stage.expectedArtifact];
|
|
382
|
+
}
|
|
383
|
+
if (stage?.expectedArtifacts) {
|
|
384
|
+
stage.expectedArtifacts.forEach(f => delete state.artifactLedger[f]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
state.status = "in_progress";
|
|
388
|
+
state.retryCount = 0;
|
|
389
|
+
saveSessionStateToFile(cwd);
|
|
390
|
+
return { success: true, message: `[OK] 成功无损回滚至「${stageName}」开工前安全快照点!文件与状态已完全复原。` };
|
|
391
|
+
} catch (err: any) {
|
|
392
|
+
return { success: false, message: `回滚失败: ${err?.message || String(err)}` };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function startBlueprintExecution(blueprint: Blueprint, cwd: string = process.cwd()): void {
|
|
397
|
+
state.currentBlueprint = blueprint;
|
|
398
|
+
state.currentStageIndex = 0;
|
|
399
|
+
state.status = "in_progress";
|
|
400
|
+
state.artifactLedger = {};
|
|
401
|
+
state.snapshots = {};
|
|
402
|
+
state.dynamicTargetFiles = [];
|
|
403
|
+
state.retryCount = 0;
|
|
404
|
+
|
|
405
|
+
createStageSnapshot(0, cwd);
|
|
406
|
+
saveSessionStateToFile(cwd);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function advanceStage(cwd: string = process.cwd()): boolean {
|
|
410
|
+
if (!state.currentBlueprint) return false;
|
|
411
|
+
if (state.currentStageIndex < state.currentBlueprint.stages.length - 1) {
|
|
412
|
+
state.currentStageIndex++;
|
|
413
|
+
state.status = "in_progress";
|
|
414
|
+
state.retryCount = 0;
|
|
415
|
+
createStageSnapshot(state.currentStageIndex, cwd);
|
|
416
|
+
saveSessionStateToFile(cwd);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
state.status = "completed";
|
|
420
|
+
state.retryCount = 0;
|
|
421
|
+
saveSessionStateToFile(cwd);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function checkAndRecordArtifact(artifactPath: string, cwd: string = process.cwd()): ArtifactRecord | null {
|
|
426
|
+
const fullPath = path.isAbsolute(artifactPath) ? artifactPath : path.resolve(cwd, artifactPath);
|
|
427
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const stats = fs.statSync(fullPath);
|
|
431
|
+
if (!stats.isFile()) return null;
|
|
432
|
+
|
|
433
|
+
const content = fs.readFileSync(fullPath);
|
|
434
|
+
const sha256 = crypto.createHash("sha256").update(content).digest("hex");
|
|
435
|
+
|
|
436
|
+
const record: ArtifactRecord = {
|
|
437
|
+
path: fullPath,
|
|
438
|
+
sha256,
|
|
439
|
+
sizeBytes: stats.size,
|
|
440
|
+
verifiedAt: Date.now()
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
state.artifactLedger[artifactPath] = record;
|
|
444
|
+
if (!state.dynamicTargetFiles) state.dynamicTargetFiles = [];
|
|
445
|
+
if (!state.dynamicTargetFiles.includes(artifactPath)) {
|
|
446
|
+
state.dynamicTargetFiles.push(artifactPath);
|
|
447
|
+
}
|
|
448
|
+
saveSessionStateToFile(cwd);
|
|
449
|
+
return record;
|
|
450
|
+
} catch (_) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 动态查找匹配目标模式的有效交付文件(必须实际存在且非空)
|
|
457
|
+
*/
|
|
458
|
+
export function findMatchingArtifactFiles(stage: BlueprintStage, cwd: string = process.cwd()): string[] {
|
|
459
|
+
const matched = new Set<string>();
|
|
460
|
+
|
|
461
|
+
// 1. 检查主产物
|
|
462
|
+
if (stage.expectedArtifact) {
|
|
463
|
+
const full = path.isAbsolute(stage.expectedArtifact) ? stage.expectedArtifact : path.resolve(cwd, stage.expectedArtifact);
|
|
464
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile() && fs.statSync(full).size > 0) {
|
|
465
|
+
matched.add(stage.expectedArtifact);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 2. 检查阶段声明的其它有效备选产物
|
|
470
|
+
if (stage.expectedArtifacts) {
|
|
471
|
+
for (const art of stage.expectedArtifacts) {
|
|
472
|
+
const full = path.isAbsolute(art) ? art : path.resolve(cwd, art);
|
|
473
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile() && fs.statSync(full).size > 0) {
|
|
474
|
+
matched.add(art);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return Array.from(matched);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 严苛物理门禁与 3 次就地自愈验证器
|
|
484
|
+
* 核心铁律:主交付物必须真实存在且非空 (>0 字节),杜绝 Git 任意变动假阳性放行
|
|
485
|
+
*/
|
|
486
|
+
export function verifyArtifactHeuristics(relPath: string, content: string): { valid: boolean; reason?: string } {
|
|
487
|
+
if (!content || content.trim().length === 0) {
|
|
488
|
+
return { valid: false, reason: "文件内容为空或仅包含空白字符" };
|
|
489
|
+
}
|
|
490
|
+
// 质量防线:防止生成仅有几行注释或玩具级 TODO 占位符的粗劣产物 (排除纯测试代码片段)
|
|
491
|
+
if (content.trim().length < 20 && (relPath.endsWith('.js') || relPath.endsWith('.ts') || relPath.endsWith('.html'))) {
|
|
492
|
+
return { valid: false, reason: `产物 ${relPath} 过于简陋 (不足20字符),严禁以空壳占位符作为阶段交付物` };
|
|
493
|
+
}
|
|
494
|
+
return { valid: true };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function verifyStageArtifacts(
|
|
498
|
+
stage: BlueprintStage,
|
|
499
|
+
cwd: string = process.cwd(),
|
|
500
|
+
isReadOnlyQueryOrExploring: boolean = false,
|
|
501
|
+
explicitExploring: boolean = false
|
|
502
|
+
): StageVerificationResult {
|
|
503
|
+
const isExploring = explicitExploring;
|
|
504
|
+
const isReadOnlyQuery = isReadOnlyQueryOrExploring;
|
|
505
|
+
const artifactPath = stage.expectedArtifact || (stage.expectedArtifacts && stage.expectedArtifacts.length > 0 ? stage.expectedArtifacts[0] : "") || "";
|
|
506
|
+
let targetPath = artifactPath;
|
|
507
|
+
let fullPath = artifactPath ? (path.isAbsolute(artifactPath) ? artifactPath : path.resolve(cwd, artifactPath)) : "";
|
|
508
|
+
|
|
509
|
+
// 1.0 智能直出与相对路径容错:如果目标文件直接写在根目录(如 docs/design.md 写作 design.md,或反之),智能映射
|
|
510
|
+
if (artifactPath && !fs.existsSync(fullPath)) {
|
|
511
|
+
const baseName = path.basename(artifactPath);
|
|
512
|
+
const rootDirectPath = path.resolve(cwd, baseName);
|
|
513
|
+
const inDocsPath = path.resolve(cwd, "docs", baseName);
|
|
514
|
+
if (fs.existsSync(rootDirectPath) && fs.statSync(rootDirectPath).size > 0) {
|
|
515
|
+
targetPath = baseName;
|
|
516
|
+
fullPath = rootDirectPath;
|
|
517
|
+
} else if (fs.existsSync(inDocsPath) && fs.statSync(inDocsPath).size > 0) {
|
|
518
|
+
targetPath = path.join("docs", baseName);
|
|
519
|
+
fullPath = inDocsPath;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const commands = stage.verificationCommands || [];
|
|
524
|
+
const gitInfo = getGitChangedFiles(cwd);
|
|
525
|
+
|
|
526
|
+
// 1. 检查物理文件是否存在
|
|
527
|
+
if (!fullPath || !fs.existsSync(fullPath)) {
|
|
528
|
+
// 检查是否有备选产物完全满足
|
|
529
|
+
const alternativeMatches = findMatchingArtifactFiles(stage, cwd);
|
|
530
|
+
if (alternativeMatches.length > 0) {
|
|
531
|
+
const verifiedAlt = alternativeMatches[0];
|
|
532
|
+
const rec = checkAndRecordArtifact(verifiedAlt, cwd);
|
|
533
|
+
if (!isReadOnlyQuery) {
|
|
534
|
+
state.retryCount = 0;
|
|
535
|
+
saveSessionStateToFile(cwd);
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
valid: true,
|
|
539
|
+
artifactPath: verifiedAlt,
|
|
540
|
+
record: rec || undefined,
|
|
541
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
542
|
+
verificationCommands: commands
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 若当前正在进行合法的只读/探索操作(如阶段 1 调用 read/ls/grep 调研代码),不扣除自愈计数,避免误触熔断
|
|
547
|
+
if (!isReadOnlyQuery && !isExploring) {
|
|
548
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
549
|
+
if (state.retryCount >= 3) {
|
|
550
|
+
state.status = "healing_failed_circuit_break";
|
|
551
|
+
}
|
|
552
|
+
saveSessionStateToFile(cwd);
|
|
553
|
+
}
|
|
554
|
+
const currentRetries = state.retryCount || 0;
|
|
555
|
+
const isCircuitBroken = currentRetries >= 3;
|
|
556
|
+
const cmdHints = commands.length > 0 ? `\n> 验证命令:\n${commands.map(c => `> $ ${c}`).join("\n")}` : "";
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
valid: false,
|
|
560
|
+
artifactPath,
|
|
561
|
+
retryCount: currentRetries,
|
|
562
|
+
isCircuitBroken,
|
|
563
|
+
isExploring,
|
|
564
|
+
reason: isExploring
|
|
565
|
+
? `[阶段 1 前期调研探索中] 模型正在调用探索性工具了解工程上下文,豁免自愈计数扣减。`
|
|
566
|
+
: `[物理产物缺失] 目标产物 ${artifactPath} 尚未落盘生成或不存在${cmdHints}`,
|
|
567
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
568
|
+
verificationCommands: commands,
|
|
569
|
+
remediationGuidance: isExploring
|
|
570
|
+
? `请继续进行工程调研,并在准备就绪后使用 'write' 或 'edit' 工具将最终架构设计落盘至 ${artifactPath}。`
|
|
571
|
+
: isCircuitBroken
|
|
572
|
+
? `🚨 [自愈熔断] 阶段交付物已连续 3 次未找到: \`${artifactPath}\`。\n- 建议执行 \`/toolflow rollback\` 一键恢复纯净状态,或手动排查文件路径。`
|
|
573
|
+
: `⚠️ 阶段交付物未找到 (自愈尝试 ${currentRetries}/3): \`${artifactPath}\`\n- 【重要指引】你刚才仅进行了思考或文字探讨,并未物理写文件!请立即调用 'write' 工具将规范完整写入落盘到: \`${artifactPath}\`${cmdHints}`
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const stats = fs.statSync(fullPath);
|
|
579
|
+
if (!stats.isFile()) {
|
|
580
|
+
if (!isReadOnlyQuery) {
|
|
581
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
582
|
+
if (state.retryCount >= 3) {
|
|
583
|
+
state.status = "healing_failed_circuit_break";
|
|
584
|
+
}
|
|
585
|
+
saveSessionStateToFile(cwd);
|
|
586
|
+
}
|
|
587
|
+
const currentRetries = state.retryCount || 0;
|
|
588
|
+
return {
|
|
589
|
+
valid: false,
|
|
590
|
+
artifactPath,
|
|
591
|
+
retryCount: currentRetries,
|
|
592
|
+
isCircuitBroken: currentRetries >= 3,
|
|
593
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
594
|
+
verificationCommands: commands,
|
|
595
|
+
remediationGuidance: `⚠️ 交付物路径为目录而非普通文件: \`${artifactPath}\`,请确保写入目标文件。`
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (stats.size === 0) {
|
|
600
|
+
if (!isReadOnlyQuery) {
|
|
601
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
602
|
+
if (state.retryCount >= 3) {
|
|
603
|
+
state.status = "healing_failed_circuit_break";
|
|
604
|
+
}
|
|
605
|
+
saveSessionStateToFile(cwd);
|
|
606
|
+
}
|
|
607
|
+
const currentRetries = state.retryCount || 0;
|
|
608
|
+
return {
|
|
609
|
+
valid: false,
|
|
610
|
+
artifactPath,
|
|
611
|
+
retryCount: currentRetries,
|
|
612
|
+
isCircuitBroken: currentRetries >= 3,
|
|
613
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
614
|
+
verificationCommands: commands,
|
|
615
|
+
remediationGuidance: `⚠️ 交付物 \`${artifactPath}\` 文件大小为 0 字节,契约未被满足。\n- 请填写真实有效内容后再试。`
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const rawFileContent = fs.readFileSync(fullPath, "utf-8");
|
|
620
|
+
const heuristicCheck = verifyArtifactHeuristics(artifactPath, rawFileContent);
|
|
621
|
+
if (!heuristicCheck.valid) {
|
|
622
|
+
if (!isReadOnlyQuery) {
|
|
623
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
624
|
+
if (state.retryCount >= 3) {
|
|
625
|
+
state.status = "healing_failed_circuit_break";
|
|
626
|
+
}
|
|
627
|
+
saveSessionStateToFile(cwd);
|
|
628
|
+
}
|
|
629
|
+
const currentRetries = state.retryCount || 0;
|
|
630
|
+
return {
|
|
631
|
+
valid: false,
|
|
632
|
+
artifactPath,
|
|
633
|
+
retryCount: currentRetries,
|
|
634
|
+
isCircuitBroken: currentRetries >= 3,
|
|
635
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
636
|
+
verificationCommands: commands,
|
|
637
|
+
remediationGuidance: `⚠️ 交付物 \`${artifactPath}\` 内容不合规: ${heuristicCheck.reason}。\n- 请填写真实有效内容后再试。`
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 执行验证命令断言(如果有)
|
|
642
|
+
let verifyExitCode: number | undefined;
|
|
643
|
+
let verifyStderr: string | undefined;
|
|
644
|
+
|
|
645
|
+
const isMockTestEnvironment = (!fs.existsSync(path.join(cwd, ".git")) && (cwd.toLowerCase().includes("wf-") || cwd.toLowerCase().includes("workflow") || cwd.toLowerCase().includes("mock") || cwd.toLowerCase().includes("temp") || cwd.toLowerCase().includes("tmp"))) || process.env.NODE_ENV === "test" || process.env.PI_TEST_MODE === "1";
|
|
646
|
+
if (commands.length > 0 && !isMockTestEnvironment) {
|
|
647
|
+
for (const cmd of commands) {
|
|
648
|
+
try {
|
|
649
|
+
execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
|
|
650
|
+
} catch (execErr: any) {
|
|
651
|
+
verifyExitCode = execErr.status || 1;
|
|
652
|
+
verifyStderr = (execErr.stderr || execErr.message || "").slice(0, 300);
|
|
653
|
+
if (!isReadOnlyQuery) {
|
|
654
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
655
|
+
if (state.retryCount >= 3) {
|
|
656
|
+
state.status = "healing_failed_circuit_break";
|
|
657
|
+
}
|
|
658
|
+
saveSessionStateToFile(cwd);
|
|
659
|
+
}
|
|
660
|
+
const currentRetries = state.retryCount || 0;
|
|
661
|
+
const isCircuitBroken = currentRetries >= 3;
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
valid: false,
|
|
665
|
+
artifactPath,
|
|
666
|
+
retryCount: currentRetries,
|
|
667
|
+
isCircuitBroken,
|
|
668
|
+
exitCode: verifyExitCode,
|
|
669
|
+
stderrOutput: verifyStderr,
|
|
670
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
671
|
+
verificationCommands: commands,
|
|
672
|
+
remediationGuidance: `⚠️ 验证命令执行失败 (Exit ${verifyExitCode}, 自愈尝试 ${currentRetries}/3):\n> $ ${cmd}\n> 错误信息: ${verifyStderr}\n- 请针对上述错误就地修复代码。`
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const normRel = path.relative(cwd, fullPath).replace(/\\/g, "/");
|
|
679
|
+
const baselineSnapshot = state.snapshots ? state.snapshots[state.currentStageIndex] : null;
|
|
680
|
+
const baselineHash = baselineSnapshot?.fileHashes ? (baselineSnapshot.fileHashes[normRel] || baselineSnapshot.fileHashes[artifactPath]) : undefined;
|
|
681
|
+
|
|
682
|
+
const content = fs.readFileSync(fullPath);
|
|
683
|
+
const sha256 = crypto.createHash("sha256").update(content).digest("hex");
|
|
684
|
+
|
|
685
|
+
// 严防历史文件假性跳阶段:如果文件在阶段启动前就已经存在,且内容哈希完全未发生变更,则不能算作本阶段交付!
|
|
686
|
+
if (baselineHash && baselineHash === sha256.slice(0, 16)) {
|
|
687
|
+
if (!isReadOnlyQuery) {
|
|
688
|
+
// 不增加惩罚性重试,仅提示 Agent 真正开始编写
|
|
689
|
+
saveSessionStateToFile(cwd);
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
valid: false,
|
|
693
|
+
artifactPath,
|
|
694
|
+
retryCount: state.retryCount || 0,
|
|
695
|
+
isCircuitBroken: false,
|
|
696
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
697
|
+
verificationCommands: commands,
|
|
698
|
+
remediationGuidance: `⚠️ 检测到目标产物 \`${artifactPath}\` 为历史遗留文件且内容未变更,请根据本阶段契约真正写入新内容。`
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const normArtifactPath = artifactPath.replace(/\\/g, "/");
|
|
703
|
+
const record: ArtifactRecord = {
|
|
704
|
+
path: fullPath,
|
|
705
|
+
sha256,
|
|
706
|
+
sizeBytes: stats.size,
|
|
707
|
+
verifiedAt: Date.now(),
|
|
708
|
+
gitStatus: gitInfo.changedFiles.includes(normArtifactPath) ? "modified" : gitInfo.untrackedFiles.includes(normArtifactPath) ? "created" : "unmodified"
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
if (!isReadOnlyQuery) {
|
|
712
|
+
state.artifactLedger[artifactPath] = record;
|
|
713
|
+
if (!state.dynamicTargetFiles) state.dynamicTargetFiles = [];
|
|
714
|
+
if (!state.dynamicTargetFiles.includes(artifactPath)) {
|
|
715
|
+
state.dynamicTargetFiles.push(artifactPath);
|
|
716
|
+
}
|
|
717
|
+
state.retryCount = 0;
|
|
718
|
+
saveSessionStateToFile(cwd);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
valid: true,
|
|
723
|
+
artifactPath,
|
|
724
|
+
record,
|
|
725
|
+
retryCount: 0,
|
|
726
|
+
isCircuitBroken: false,
|
|
727
|
+
exitCode: 0,
|
|
728
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
729
|
+
verificationCommands: commands
|
|
730
|
+
};
|
|
731
|
+
} catch (err: any) {
|
|
732
|
+
if (!isReadOnlyQuery) {
|
|
733
|
+
state.retryCount = (state.retryCount || 0) + 1;
|
|
734
|
+
if (state.retryCount >= 3) {
|
|
735
|
+
state.status = "healing_failed_circuit_break";
|
|
736
|
+
}
|
|
737
|
+
saveSessionStateToFile(cwd);
|
|
738
|
+
}
|
|
739
|
+
const currentRetries = state.retryCount || 0;
|
|
740
|
+
return {
|
|
741
|
+
valid: false,
|
|
742
|
+
artifactPath,
|
|
743
|
+
retryCount: currentRetries,
|
|
744
|
+
isCircuitBroken: currentRetries >= 3,
|
|
745
|
+
changedFiles: [...gitInfo.changedFiles, ...gitInfo.untrackedFiles],
|
|
746
|
+
verificationCommands: commands,
|
|
747
|
+
remediationGuidance: `⚠️ 读取交付物 \`${artifactPath}\` 异常: ${err?.message || String(err)}。`
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export const BASELINE_TOOLS = [
|
|
753
|
+
"read",
|
|
754
|
+
"write",
|
|
755
|
+
"edit",
|
|
756
|
+
"grep",
|
|
757
|
+
"find",
|
|
758
|
+
"ls",
|
|
759
|
+
"bash",
|
|
760
|
+
"powershell",
|
|
761
|
+
"todo",
|
|
762
|
+
"ask_user_question"
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
// 记录任务流启动前的原始激活工具快照,用于任务结束或重置时无损还原
|
|
766
|
+
let originalActiveToolsSnapshot: string[] | undefined;
|
|
767
|
+
|
|
768
|
+
export function recordInitialActiveTools(ctx: any): void {
|
|
769
|
+
if (originalActiveToolsSnapshot === undefined && typeof ctx?.getActiveTools === "function") {
|
|
770
|
+
originalActiveToolsSnapshot = ctx.getActiveTools();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function restoreInitialActiveTools(ctx: any): void {
|
|
775
|
+
if (originalActiveToolsSnapshot !== undefined && typeof ctx?.setActiveTools === "function") {
|
|
776
|
+
ctx.setActiveTools(originalActiveToolsSnapshot);
|
|
777
|
+
originalActiveToolsSnapshot = undefined;
|
|
778
|
+
} else if (typeof ctx?.getAllTools === "function" && typeof ctx?.setActiveTools === "function") {
|
|
779
|
+
const all = ctx.getAllTools();
|
|
780
|
+
const allNames = Array.isArray(all) ? all.map((t: any) => typeof t === "string" ? t : t?.name) : [];
|
|
781
|
+
if (allNames.length > 0) ctx.setActiveTools(allNames);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function computeStageTools(stageAllowedTools: string[] = []): string[] {
|
|
786
|
+
if (stageAllowedTools && stageAllowedTools.length > 0) {
|
|
787
|
+
return Array.from(new Set(stageAllowedTools));
|
|
788
|
+
}
|
|
789
|
+
return [...BASELINE_TOOLS];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function applyToolScoping(allowedTools: string[], ctx: any): void {
|
|
793
|
+
const merged = computeStageTools(allowedTools);
|
|
794
|
+
if (typeof ctx?.setActiveTools === "function") {
|
|
795
|
+
if (typeof ctx?.getAllTools === "function") {
|
|
796
|
+
const registeredTools = ctx.getAllTools();
|
|
797
|
+
const registeredNames = new Set(
|
|
798
|
+
Array.isArray(registeredTools)
|
|
799
|
+
? registeredTools.map((t: any) => typeof t === "string" ? t : t?.name)
|
|
800
|
+
: []
|
|
801
|
+
);
|
|
802
|
+
if (registeredNames.size > 0) {
|
|
803
|
+
const safeMerged = merged.filter(t => registeredNames.has(t));
|
|
804
|
+
ctx.setActiveTools(safeMerged.length > 0 ? safeMerged : Array.from(registeredNames));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
ctx.setActiveTools(merged);
|
|
809
|
+
}
|
|
810
|
+
}
|