@winspan/claude-forge 0.9.4 → 1.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.
- package/dist/ai-gateway/token-budget.d.ts +6 -0
- package/dist/ai-gateway/token-budget.d.ts.map +1 -1
- package/dist/ai-gateway/token-budget.js +16 -0
- package/dist/ai-gateway/token-budget.js.map +1 -1
- package/dist/convention/types.d.ts +6 -0
- package/dist/convention/types.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use-handler.d.ts.map +1 -1
- package/dist/daemon/handlers/post-tool-use-handler.js +6 -1
- package/dist/daemon/handlers/post-tool-use-handler.js.map +1 -1
- package/dist/daemon/handlers/pre-tool-use-handler.d.ts +2 -0
- package/dist/daemon/handlers/pre-tool-use-handler.d.ts.map +1 -1
- package/dist/daemon/handlers/pre-tool-use-handler.js +14 -0
- package/dist/daemon/handlers/pre-tool-use-handler.js.map +1 -1
- package/dist/pipeline/index.d.ts +5 -0
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +65 -3
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/phase-manager.d.ts +36 -5
- package/dist/pipeline/phase-manager.d.ts.map +1 -1
- package/dist/pipeline/phase-manager.js +254 -191
- package/dist/pipeline/phase-manager.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { PHASE_LABELS, nextPhase } from './types.js';
|
|
2
2
|
import { logger } from '../utils/logger.js';
|
|
3
3
|
import { memoryManager } from '../utils/memory-manager.js';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
export class PhaseManager {
|
|
5
|
-
taskCompletionDetector;
|
|
6
7
|
store;
|
|
7
8
|
knowledgeContext = memoryManager.register('phase:knowledge', { max: 100, ttl: 24 * 3600_000 });
|
|
8
9
|
totalPhaseEventCounts = memoryManager.register('phase:eventCounts', { max: 100, ttl: 24 * 3600_000 });
|
|
@@ -13,10 +14,13 @@ export class PhaseManager {
|
|
|
13
14
|
phaseWriteEditCounts = memoryManager.register('phase:writeEditCounts', { max: 100, ttl: 24 * 3600_000 });
|
|
14
15
|
/** 追踪每个 pipeline:phase 最近 5 次工具调用 */
|
|
15
16
|
recentTools = memoryManager.register('phase:recentTools', { max: 100, ttl: 24 * 3600_000 });
|
|
17
|
+
/** 质量门禁去重缓存:已通过的 pipeline:phase 不再重复检查(24h TTL) */
|
|
18
|
+
gatePassedCache = memoryManager.register('phase:gatePassed', { max: 100, ttl: 24 * 3600_000 });
|
|
19
|
+
/** 活动阈值触发标记:标记为 true 的 pipeline:phase 跳过质量门禁直接推进 */
|
|
20
|
+
activityThresholdTriggered = memoryManager.register('phase:thresholdTriggered', { max: 100, ttl: 24 * 3600_000 });
|
|
16
21
|
static MAX_FIX_ATTEMPTS = 3;
|
|
17
22
|
static EMPTY_PHASE_ADVANCE_THRESHOLD = 3;
|
|
18
|
-
constructor(store
|
|
19
|
-
this.taskCompletionDetector = taskCompletionDetector;
|
|
23
|
+
constructor(store) {
|
|
20
24
|
this.store = store;
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
@@ -114,6 +118,11 @@ export class PhaseManager {
|
|
|
114
118
|
// 注入任务完成信号说明(让 Claude 主动确认完成,而非被动猜测)
|
|
115
119
|
parts.push(`完成后执行:echo "FORGE_TASK_DONE: ${currentTask.id}"`);
|
|
116
120
|
}
|
|
121
|
+
// 阶段指导:注入当前阶段的具体工作指导(让 Claude 知道该做什么)
|
|
122
|
+
const directive = this.getPhaseDirective(phase, currentTask, pipeline);
|
|
123
|
+
if (directive) {
|
|
124
|
+
parts.push(`\n${directive}`);
|
|
125
|
+
}
|
|
117
126
|
// 待完成任务(仅显示前 3 个,超过则省略)
|
|
118
127
|
const pendingTasks = phaseTasks.filter(t => t.status !== 'completed');
|
|
119
128
|
if (pendingTasks.length > 1) {
|
|
@@ -124,217 +133,259 @@ export class PhaseManager {
|
|
|
124
133
|
return parts.join(' ');
|
|
125
134
|
}
|
|
126
135
|
/**
|
|
127
|
-
* 根据 PostToolUse
|
|
136
|
+
* 根据 PostToolUse 事件判断是否推进阶段(重构版:职责分离 + 信号优先)
|
|
128
137
|
*/
|
|
129
138
|
async checkAndAdvance(pipeline, event, qualityGate) {
|
|
139
|
+
const phase = pipeline.phase;
|
|
140
|
+
// 1. 工具追踪
|
|
141
|
+
this.trackToolUsage(pipeline, event);
|
|
142
|
+
// 2. 空阶段自愈
|
|
143
|
+
const emptyPhaseResult = this.handleEmptyPhase(pipeline);
|
|
144
|
+
if (emptyPhaseResult)
|
|
145
|
+
return emptyPhaseResult;
|
|
146
|
+
// 3. 任务完成检测
|
|
147
|
+
this.detectAndMarkCompletion(pipeline, event);
|
|
148
|
+
// 4. 活动阈值检测
|
|
149
|
+
const thresholdResult = this.handleActivityThreshold(pipeline);
|
|
150
|
+
// 5. 检查阶段是否全部完成
|
|
151
|
+
const progress = this.store.getPhaseProgress(pipeline.id, phase);
|
|
152
|
+
if (progress.total === 0 || progress.completed < progress.total) {
|
|
153
|
+
return { advanced: false };
|
|
154
|
+
}
|
|
155
|
+
// 6. 质量门禁(仅首次完成时触发,活动阈值触发时跳过)
|
|
156
|
+
if (qualityGate) {
|
|
157
|
+
const gateResult = await this.runQualityGateOnce(pipeline, event, qualityGate);
|
|
158
|
+
if (gateResult.blocked) {
|
|
159
|
+
return {
|
|
160
|
+
advanced: false,
|
|
161
|
+
qualityBlocked: true,
|
|
162
|
+
qualityIssues: gateResult.issues,
|
|
163
|
+
qualityLevel: gateResult.level,
|
|
164
|
+
fixAttempts: gateResult.fixAttempts,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// 7. 阶段推进
|
|
169
|
+
return this.advancePhase(pipeline, thresholdResult?.message);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 1. 工具追踪:记录工具类别、调用次数、最近工具
|
|
173
|
+
*/
|
|
174
|
+
trackToolUsage(pipeline, event) {
|
|
130
175
|
const phase = pipeline.phase;
|
|
131
176
|
const toolName = event.tool_name || '';
|
|
132
177
|
const inputStr = event.tool_input ? JSON.stringify(event.tool_input) : '';
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const toolsStr = this.phaseToolsSeen.get(
|
|
178
|
+
const key = `${pipeline.id}:${phase}`;
|
|
179
|
+
// 工具类别追踪
|
|
180
|
+
const toolsStr = this.phaseToolsSeen.get(key) ?? '';
|
|
136
181
|
const toolsSeenSet = toolsStr ? new Set(toolsStr.split(',')) : new Set();
|
|
137
182
|
const toolCategory = this.categorizeToolCall(toolName, inputStr);
|
|
138
183
|
if (toolCategory)
|
|
139
184
|
toolsSeenSet.add(toolCategory);
|
|
140
|
-
this.phaseToolsSeen.set(
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
this.phaseToolsSeen.set(key, [...toolsSeenSet].join(','));
|
|
186
|
+
// 调用次数追踪
|
|
187
|
+
const eventCount = this.totalPhaseEventCounts.get(key) || 0;
|
|
188
|
+
this.totalPhaseEventCounts.set(key, eventCount + 1);
|
|
189
|
+
// Write/Edit 次数追踪
|
|
190
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
191
|
+
const writeEditCount = this.phaseWriteEditCounts.get(key) || 0;
|
|
192
|
+
this.phaseWriteEditCounts.set(key, writeEditCount + 1);
|
|
193
|
+
}
|
|
194
|
+
// 最近 5 次工具追踪
|
|
195
|
+
const recent = this.recentTools.get(key) || [];
|
|
196
|
+
recent.push(toolName);
|
|
197
|
+
if (recent.length > 5)
|
|
198
|
+
recent.shift();
|
|
199
|
+
this.recentTools.set(key, recent);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 2. 空阶段自愈:当前阶段无任务时自动推进
|
|
203
|
+
*/
|
|
204
|
+
handleEmptyPhase(pipeline) {
|
|
205
|
+
const phase = pipeline.phase;
|
|
206
|
+
const progress = this.store.getPhaseProgress(pipeline.id, phase);
|
|
207
|
+
const key = `${pipeline.id}:${phase}`;
|
|
208
|
+
const eventCount = this.totalPhaseEventCounts.get(key) || 0;
|
|
209
|
+
if (progress.total === 0 && eventCount >= PhaseManager.EMPTY_PHASE_ADVANCE_THRESHOLD) {
|
|
210
|
+
logger.warn(`[Pipeline] 阶段 ${PHASE_LABELS[phase]} 无任务(phase 标签可能无效),自动推进到下一阶段`);
|
|
211
|
+
const newPh = nextPhase(phase, pipeline.plannedPhases);
|
|
212
|
+
this.store.updatePhase(pipeline.id, newPh);
|
|
213
|
+
this.phaseToolsSeen.delete(key);
|
|
214
|
+
this.gatePassedCache.delete(key);
|
|
215
|
+
this.activityThresholdTriggered.delete(key);
|
|
216
|
+
return { advanced: true, newPhase: newPh };
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 3. 任务完成检测:优先显式信号,回退到启发式
|
|
222
|
+
*/
|
|
223
|
+
detectAndMarkCompletion(pipeline, event) {
|
|
224
|
+
const phase = pipeline.phase;
|
|
225
|
+
const toolName = event.tool_name || '';
|
|
226
|
+
const inputStr = event.tool_input ? JSON.stringify(event.tool_input) : '';
|
|
143
227
|
const pendingTasks = pipeline.tasks.filter(t => t.phase === phase && (t.status === 'in_progress' || t.status === 'pending'));
|
|
144
228
|
for (const currentTask of pendingTasks) {
|
|
145
|
-
const completed = this.detectTaskCompletion(phase, toolName, inputStr, event,
|
|
229
|
+
const completed = this.detectTaskCompletion(phase, toolName, inputStr, event, currentTask, pipeline.id);
|
|
146
230
|
if (completed) {
|
|
147
|
-
// 生成成果摘要:工具名 + 操作对象(文件路径或关键信息)
|
|
148
231
|
const artifact = this.summarizeArtifact(toolName, inputStr, event);
|
|
149
232
|
this.store.updateTaskStatus(currentTask.id, 'completed', artifact);
|
|
150
233
|
logger.info(`[Pipeline] 任务已完成:${currentTask.title} | 成果:${artifact}`);
|
|
151
234
|
break; // 每次事件只完成一个任务
|
|
152
235
|
}
|
|
153
236
|
}
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 4. 活动阈值检测:达到阈值后强制完成剩余任务
|
|
240
|
+
*/
|
|
241
|
+
handleActivityThreshold(pipeline) {
|
|
242
|
+
const phase = pipeline.phase;
|
|
156
243
|
const progress = this.store.getPhaseProgress(pipeline.id, phase);
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const writeEditKey = `${pipeline.id}:${phase}`;
|
|
163
|
-
const writeEditCount = this.phaseWriteEditCounts.get(writeEditKey) || 0;
|
|
164
|
-
this.phaseWriteEditCounts.set(writeEditKey, writeEditCount + 1);
|
|
165
|
-
}
|
|
166
|
-
// 空阶段自愈:当前阶段无任务(可能因 AI 返回了无效 phase 标签),
|
|
167
|
-
// 累积足够工具调用后自动推进,避免 Pipeline 永久卡死
|
|
168
|
-
if (progress.total === 0 && phaseEventCount + 1 >= PhaseManager.EMPTY_PHASE_ADVANCE_THRESHOLD) {
|
|
169
|
-
logger.warn(`[Pipeline] 阶段 ${PHASE_LABELS[phase]} 无任务(phase 标签可能无效),自动推进到下一阶段`);
|
|
170
|
-
const newPh = nextPhase(phase, pipeline.plannedPhases);
|
|
171
|
-
this.store.updatePhase(pipeline.id, newPh);
|
|
172
|
-
this.phaseToolsSeen.delete(`${pipeline.id}:${phase}`);
|
|
173
|
-
return { advanced: true, newPhase: newPh };
|
|
174
|
-
}
|
|
175
|
-
// 追踪最近 5 次工具调用(供 TaskCompletionDetector 使用)
|
|
176
|
-
const recentKey = `${pipeline.id}:${phase}`;
|
|
177
|
-
const recent = this.recentTools.get(recentKey) || [];
|
|
178
|
-
recent.push(toolName);
|
|
179
|
-
if (recent.length > 5)
|
|
180
|
-
recent.shift();
|
|
181
|
-
this.recentTools.set(recentKey, recent);
|
|
182
|
-
// LLM 语义检测:在达到阈值前尝试智能判断是否完成(非阻塞,结果在下次事件中生效)
|
|
183
|
-
if (this.taskCompletionDetector && progress.completed < progress.total && phaseEventCount + 1 >= Math.floor(activityThreshold * 0.7)) {
|
|
184
|
-
// 异步触发,不阻塞当前事件处理
|
|
185
|
-
this.taskCompletionDetector.detect({
|
|
186
|
-
requirement: pipeline.requirement,
|
|
187
|
-
currentPhase: phase,
|
|
188
|
-
recentTools: recent,
|
|
189
|
-
toolCallCount: phaseEventCount + 1,
|
|
190
|
-
completedTaskCount: progress.completed,
|
|
191
|
-
totalTaskCount: progress.total,
|
|
192
|
-
}).then(detectionResult => {
|
|
193
|
-
if (detectionResult.isComplete && detectionResult.confidence !== 'low') {
|
|
194
|
-
logger.info(`[Pipeline] LLM 检测到 ${PHASE_LABELS[phase]} 阶段已完成(${detectionResult.confidence}),标记剩余任务`);
|
|
195
|
-
const remaining = pipeline.tasks.filter(t => t.phase === phase && t.status !== 'completed');
|
|
196
|
-
for (const t of remaining) {
|
|
197
|
-
this.store.updateTaskStatus(t.id, 'completed', `LLM 检测完成:${detectionResult.reason}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}).catch(err => {
|
|
201
|
-
logger.warn(`[Pipeline] LLM 任务完成检测失败,降级到阈值逻辑:${err}`);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
// 兜底推进:活动阈值强制完成(LLM 检测失败或未启用时的保底机制)
|
|
205
|
-
let forcedCompletionMessage;
|
|
206
|
-
if (progress.completed < progress.total && phaseEventCount + 1 >= activityThreshold) {
|
|
207
|
-
logger.info(`[Pipeline] 活动阈值(${activityThreshold})已达到,强制完成 ${PHASE_LABELS[phase]} 阶段剩余任务`);
|
|
244
|
+
const key = `${pipeline.id}:${phase}`;
|
|
245
|
+
const eventCount = this.totalPhaseEventCounts.get(key) || 0;
|
|
246
|
+
const threshold = this.getPhaseActivityThreshold(phase, pipeline.complexity);
|
|
247
|
+
if (progress.completed < progress.total && eventCount >= threshold) {
|
|
248
|
+
logger.info(`[Pipeline] 活动阈值(${threshold})已达到,强制完成 ${PHASE_LABELS[phase]} 阶段剩余任务`);
|
|
208
249
|
const remaining = pipeline.tasks.filter(t => t.phase === phase && t.status !== 'completed');
|
|
209
250
|
for (const t of remaining) {
|
|
210
|
-
this.store.updateTaskStatus(t.id, 'completed');
|
|
251
|
+
this.store.updateTaskStatus(t.id, 'completed', `活动阈值强制完成(${eventCount}次工具调用)`);
|
|
211
252
|
}
|
|
212
|
-
//
|
|
253
|
+
// 标记活动阈值触发,跳过质量门禁
|
|
254
|
+
this.activityThresholdTriggered.set(key, true);
|
|
213
255
|
const remainingTitles = remaining.map(t => `- ${t.title}`).join('\n');
|
|
214
|
-
|
|
256
|
+
return {
|
|
257
|
+
message: `[Forge 自动推进] ℹ️ ${PHASE_LABELS[phase]}阶段活动充分(${eventCount} 次工具调用),自动完成剩余任务:
|
|
215
258
|
|
|
216
259
|
${remainingTitles}
|
|
217
260
|
|
|
218
|
-
|
|
261
|
+
继续下一阶段工作。`
|
|
262
|
+
};
|
|
219
263
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 5. 质量门禁(去重版):已通过的阶段不再重复检查,活动阈值触发时跳过
|
|
268
|
+
*/
|
|
269
|
+
async runQualityGateOnce(pipeline, event, qualityGate) {
|
|
270
|
+
const phase = pipeline.phase;
|
|
271
|
+
const cacheKey = `${pipeline.id}:${phase}`;
|
|
272
|
+
// 活动阈值触发时跳过质量门禁
|
|
273
|
+
if (this.activityThresholdTriggered.get(cacheKey)) {
|
|
274
|
+
logger.info(`[Pipeline] 活动阈值触发,跳过质量门禁,直接推进`);
|
|
275
|
+
return { blocked: false, level: 'pass' };
|
|
276
|
+
}
|
|
277
|
+
// 已通过,跳过
|
|
278
|
+
if (this.gatePassedCache.has(cacheKey)) {
|
|
279
|
+
return { blocked: false, level: 'pass' };
|
|
280
|
+
}
|
|
281
|
+
// 首次检查
|
|
282
|
+
const fixKey = `${pipeline.id}:${phase}`;
|
|
283
|
+
const fixAttempts = this.phaseFixAttempts.get(fixKey) || 0;
|
|
284
|
+
const phaseTasks = pipeline.tasks.filter(t => t.phase === phase);
|
|
285
|
+
const taskContext = phaseTasks
|
|
286
|
+
.map(t => `- ${t.title}: ${t.output || '(进行中)'}`)
|
|
287
|
+
.join('\n');
|
|
288
|
+
const recentFiles = this.getRecentlyModifiedFiles(pipeline.projectPath, 30);
|
|
289
|
+
const recentFilesNote = recentFiles.length > 0
|
|
290
|
+
? `\n\n## 最近修改的文件(30分钟内)\n${recentFiles.map(f => `- ${f}`).join('\n')}`
|
|
291
|
+
: '';
|
|
292
|
+
const phaseContext = taskContext + recentFilesNote;
|
|
293
|
+
let gateResult = (phase === 'analyze' || phase === 'design')
|
|
294
|
+
? await qualityGate.reviewPhaseCompletion(phase, pipeline.requirement, phaseContext)
|
|
295
|
+
: await qualityGate.reviewForPhase(event, phase);
|
|
296
|
+
if (gateResult === null && phase !== 'analyze' && phase !== 'design') {
|
|
297
|
+
gateResult = await qualityGate.reviewPhaseCompletion(phase, pipeline.requirement, phaseContext);
|
|
298
|
+
}
|
|
299
|
+
if (gateResult && gateResult.level === 'fail') {
|
|
300
|
+
const newAttempts = fixAttempts + 1;
|
|
301
|
+
this.phaseFixAttempts.set(fixKey, newAttempts);
|
|
302
|
+
const failIssues = gateResult.checks
|
|
303
|
+
.filter(c => c.level === 'fail')
|
|
304
|
+
.map(c => `[${c.category}] ${c.message}${c.suggestion ? `\n 修复建议:${c.suggestion}` : ''}`);
|
|
305
|
+
if (newAttempts >= PhaseManager.MAX_FIX_ATTEMPTS) {
|
|
306
|
+
logger.warn(`[Pipeline] 质量门禁阶段 ${PHASE_LABELS[phase]} 已达最大修复次数(${PhaseManager.MAX_FIX_ATTEMPTS}),强制放行`);
|
|
307
|
+
this.phaseFixAttempts.delete(fixKey);
|
|
308
|
+
this.gatePassedCache.set(cacheKey, true);
|
|
309
|
+
return { blocked: false, level: 'warn' };
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
blocked: true,
|
|
313
|
+
issues: failIssues,
|
|
314
|
+
level: 'fail',
|
|
315
|
+
fixAttempts: newAttempts,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// 通过或警告,标记缓存
|
|
319
|
+
if (gateResult) {
|
|
320
|
+
this.gatePassedCache.set(cacheKey, true);
|
|
321
|
+
this.phaseFixAttempts.delete(fixKey);
|
|
322
|
+
}
|
|
323
|
+
return { blocked: false, level: gateResult?.level || 'pass' };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 6. 阶段推进
|
|
327
|
+
*/
|
|
328
|
+
async advancePhase(pipeline, forcedMessage) {
|
|
329
|
+
const phase = pipeline.phase;
|
|
330
|
+
const newPh = nextPhase(phase, pipeline.plannedPhases);
|
|
331
|
+
// 跨阶段补充检测
|
|
332
|
+
let gapMessage;
|
|
333
|
+
const nextProgress = this.store.getPhaseProgress(pipeline.id, newPh);
|
|
334
|
+
if (nextProgress.total === 0 && newPh !== 'done') {
|
|
335
|
+
gapMessage = `[Forge 阶段间隙] ⚠️ ${PHASE_LABELS[newPh]}阶段暂无任务,可能需要手动创建任务或等待自动推进。`;
|
|
336
|
+
logger.warn(`[Pipeline] 阶段间隙:${PHASE_LABELS[newPh]} 无任务`);
|
|
337
|
+
}
|
|
338
|
+
this.store.updatePhase(pipeline.id, newPh);
|
|
339
|
+
const oldKey = `${pipeline.id}:${phase}`;
|
|
340
|
+
this.phaseToolsSeen.delete(oldKey);
|
|
341
|
+
this.gatePassedCache.delete(oldKey);
|
|
342
|
+
this.activityThresholdTriggered.delete(oldKey);
|
|
343
|
+
const phaseTasks = pipeline.tasks.filter(t => t.phase === phase);
|
|
344
|
+
const artifacts = phaseTasks
|
|
345
|
+
.map(t => ` - ${t.title}: ${t.output || '(无记录)'}`)
|
|
346
|
+
.join('\n');
|
|
347
|
+
logger.info(`[Pipeline] 阶段推进:${PHASE_LABELS[phase]} → ${PHASE_LABELS[newPh]}\n` +
|
|
348
|
+
` 完成 ${phaseTasks.length} 个任务,成果汇总:\n${artifacts}`);
|
|
349
|
+
return { advanced: true, newPhase: newPh, gapMessage: gapMessage ?? forcedMessage };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* 获取项目目录中最近 N 分钟内修改的文件列表(相对路径)
|
|
353
|
+
* 用于增强质量门禁的 phaseContext,让 AI 有实际依据
|
|
354
|
+
*/
|
|
355
|
+
getRecentlyModifiedFiles(projectPath, minutes) {
|
|
356
|
+
const cutoff = Date.now() - minutes * 60_000;
|
|
357
|
+
const result = [];
|
|
358
|
+
const ignoreDirs = new Set(['.git', 'node_modules', 'dist', '.claude-forge', '.claude']);
|
|
359
|
+
const walk = (dir, depth) => {
|
|
360
|
+
if (depth > 4)
|
|
361
|
+
return;
|
|
362
|
+
try {
|
|
363
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
if (ignoreDirs.has(entry.name))
|
|
366
|
+
continue;
|
|
367
|
+
const fullPath = path.join(dir, entry.name);
|
|
368
|
+
if (entry.isDirectory()) {
|
|
369
|
+
walk(fullPath, depth + 1);
|
|
302
370
|
}
|
|
303
|
-
if (
|
|
304
|
-
|
|
371
|
+
else if (entry.isFile()) {
|
|
372
|
+
try {
|
|
373
|
+
const stat = fs.statSync(fullPath);
|
|
374
|
+
if (stat.mtimeMs >= cutoff) {
|
|
375
|
+
result.push(path.relative(projectPath, fullPath));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch { /* 忽略无法访问的文件 */ }
|
|
305
379
|
}
|
|
306
|
-
this.phaseFixAttempts.delete(fixKey);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const newPh = nextPhase(phase, pipeline.plannedPhases);
|
|
310
|
-
// 跨阶段补充检测:在阶段切换前检测上一阶段遗留问题
|
|
311
|
-
let gapMessage;
|
|
312
|
-
if (qualityGate?.phaseGapDetector) {
|
|
313
|
-
const gapResult = qualityGate.phaseGapDetector.detect(pipeline, phase, newPh);
|
|
314
|
-
if (gapResult.action === 'ask_user' && gapResult.systemMessage) {
|
|
315
|
-
// 暂停:需求歧义未解决,追问用户后才能推进
|
|
316
|
-
logger.info(`[Pipeline] 跨阶段检测:${PHASE_LABELS[phase]}→${PHASE_LABELS[newPh]} 发现需求歧义,暂停推进`);
|
|
317
|
-
return { advanced: false, qualityBlocked: true, systemMessage: gapResult.systemMessage };
|
|
318
|
-
}
|
|
319
|
-
if (gapResult.systemMessage) {
|
|
320
|
-
// 非阻断:注入提示,Pipeline 继续推进
|
|
321
|
-
gapMessage = gapResult.systemMessage;
|
|
322
|
-
logger.info(`[Pipeline] 跨阶段检测:${PHASE_LABELS[phase]}→${PHASE_LABELS[newPh]} 注入补充提示(${gapResult.action})`);
|
|
323
380
|
}
|
|
324
381
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const phaseTasks = pipeline.tasks.filter(t => t.phase === phase);
|
|
330
|
-
const artifacts = phaseTasks
|
|
331
|
-
.map(t => ` - ${t.title}: ${t.output || '(无记录)'}`)
|
|
332
|
-
.join('\n');
|
|
333
|
-
logger.info(`[Pipeline] 阶段推进:${PHASE_LABELS[phase]} → ${PHASE_LABELS[newPh]}\n` +
|
|
334
|
-
` 完成 ${phaseTasks.length} 个任务,成果汇总:\n${artifacts}`);
|
|
335
|
-
return { advanced: true, newPhase: newPh, gapMessage: gapMessage ?? forcedCompletionMessage, qualityLevel };
|
|
382
|
+
catch { /* 忽略无法读取的目录 */ }
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
walk(projectPath, 0);
|
|
336
386
|
}
|
|
337
|
-
|
|
387
|
+
catch { /* 忽略整体失败 */ }
|
|
388
|
+
return result.slice(0, 20); // 最多返回 20 个文件,避免 prompt 过长
|
|
338
389
|
}
|
|
339
390
|
/** 每个阶段兜底推进所需的最少工具调用次数(按复杂度动态调整) */
|
|
340
391
|
getPhaseActivityThreshold(phase, complexity) {
|
|
@@ -381,17 +432,26 @@ ${taskDesc}`;
|
|
|
381
432
|
}
|
|
382
433
|
}
|
|
383
434
|
/**
|
|
384
|
-
*
|
|
435
|
+
* 检测任务是否完成(重构版:信号优先 + 启发式回退)
|
|
385
436
|
*/
|
|
386
|
-
detectTaskCompletion(phase, toolName, inputStr, event,
|
|
437
|
+
detectTaskCompletion(phase, toolName, inputStr, event, currentTask, pipelineId) {
|
|
387
438
|
// 优先检测显式完成信号(FORGE_TASK_DONE),100% 准确,无需模式匹配
|
|
388
439
|
// 信号格式:echo "FORGE_TASK_DONE: <task_id>" 或文件注释 // FORGE: task_done <task_id>
|
|
389
440
|
if (toolName === 'Bash' && inputStr.includes('FORGE_TASK_DONE:')) {
|
|
390
|
-
|
|
441
|
+
const match = inputStr.match(/FORGE_TASK_DONE:\s*(\S+)/);
|
|
442
|
+
if (match && match[1] === currentTask.id) {
|
|
443
|
+
logger.info(`[Pipeline] 检测到显式完成信号:${currentTask.id}`);
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
391
446
|
}
|
|
392
447
|
if ((toolName === 'Write' || toolName === 'Edit') && inputStr.includes('FORGE: task_done')) {
|
|
393
|
-
|
|
448
|
+
const match = inputStr.match(/FORGE:\s*task_done\s+(\S+)/);
|
|
449
|
+
if (match && match[1] === currentTask.id) {
|
|
450
|
+
logger.info(`[Pipeline] 检测到显式完成信号:${currentTask.id}`);
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
394
453
|
}
|
|
454
|
+
// 回退到启发式检测(保留向后兼容)
|
|
395
455
|
switch (phase) {
|
|
396
456
|
case 'analyze':
|
|
397
457
|
// 分析阶段:有输出类工具(Write/Edit)即可,或明确的确认信号
|
|
@@ -399,7 +459,6 @@ ${taskDesc}`;
|
|
|
399
459
|
inputStr.includes('已分析') || inputStr.includes('analysis complete')) {
|
|
400
460
|
return true;
|
|
401
461
|
}
|
|
402
|
-
// 写入分析文档即算完成(不强制要求先探索,避免过度严格)
|
|
403
462
|
return toolName === 'Write' || toolName === 'Edit';
|
|
404
463
|
case 'design':
|
|
405
464
|
// 设计阶段:必须有架构文档写入(Write 或 Edit),支持中英文文档名
|
|
@@ -422,17 +481,21 @@ ${taskDesc}`;
|
|
|
422
481
|
inputStr.includes('.vue') || inputStr.includes('.svelte'));
|
|
423
482
|
case 'test': {
|
|
424
483
|
// 测试阶段:必须同时有测试文件写入 AND 测试执行(非 watch 模式)
|
|
484
|
+
const key = `${pipelineId}:${phase}`;
|
|
485
|
+
const toolsStr = this.phaseToolsSeen.get(key) ?? '';
|
|
486
|
+
const toolsSeenSet = toolsStr ? new Set(toolsStr.split(',')) : new Set();
|
|
425
487
|
const hasTestFile = (toolName === 'Write' || toolName === 'Edit') &&
|
|
426
488
|
(inputStr.includes('test') || inputStr.includes('spec'));
|
|
427
489
|
const hasTestRun = toolName === 'Bash' &&
|
|
428
490
|
(inputStr.includes('test') || inputStr.includes('vitest') || inputStr.includes('jest')) &&
|
|
429
491
|
!inputStr.includes('--watch');
|
|
430
|
-
//
|
|
492
|
+
// 持久化标记(trackToolUsage 已更新 phaseToolsSeen)
|
|
431
493
|
if (hasTestFile)
|
|
432
|
-
|
|
494
|
+
toolsSeenSet.add('test:file');
|
|
433
495
|
if (hasTestRun)
|
|
434
|
-
|
|
435
|
-
|
|
496
|
+
toolsSeenSet.add('test:run');
|
|
497
|
+
this.phaseToolsSeen.set(key, [...toolsSeenSet].join(','));
|
|
498
|
+
return toolsSeenSet.has('test:file') && toolsSeenSet.has('test:run');
|
|
436
499
|
}
|
|
437
500
|
case 'review':
|
|
438
501
|
// 审查阶段:有代码修改(Edit)即可(质量门禁已在 checkAndAdvance 中把关)
|