@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.
@@ -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, taskCompletionDetector) {
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
- const toolsKey = `${pipeline.id}:${phase}`;
135
- const toolsStr = this.phaseToolsSeen.get(toolsKey) ?? '';
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(toolsKey, [...toolsSeenSet].join(','));
141
- const toolsSeen = toolsSeenSet;
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, toolsSeen);
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 activityThreshold = this.getPhaseActivityThreshold(phase, pipeline.complexity);
158
- const phaseEventCount = this.totalPhaseEventCounts.get(`${pipeline.id}:${phase}`) || 0;
159
- this.totalPhaseEventCounts.set(`${pipeline.id}:${phase}`, phaseEventCount + 1);
160
- // 追踪 Write/Edit 调用次数(供 getSessionSnapshot 使用)
161
- if (toolName === 'Write' || toolName === 'Edit') {
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
- forcedCompletionMessage = `[Forge 自动推进] ℹ️ ${PHASE_LABELS[phase]}阶段活动充分(${phaseEventCount + 1} 次工具调用),自动完成剩余任务:
256
+ return {
257
+ message: `[Forge 自动推进] ℹ️ ${PHASE_LABELS[phase]}阶段活动充分(${eventCount} 次工具调用),自动完成剩余任务:
215
258
 
216
259
  ${remainingTitles}
217
260
 
218
- 继续下一阶段工作。`;
261
+ 继续下一阶段工作。`
262
+ };
219
263
  }
220
- // 检查阶段是否全部完成
221
- const updatedProgress = this.store.getPhaseProgress(pipeline.id, phase);
222
- if (updatedProgress.total > 0 && updatedProgress.completed >= updatedProgress.total) {
223
- // 质量门禁:阶段推进前检查(含自动修复循环)
224
- let qualityLevel;
225
- if (qualityGate) {
226
- const fixKey = `${pipeline.id}:${phase}`;
227
- const fixAttempts = this.phaseFixAttempts.get(fixKey) || 0;
228
- // analyze/design 阶段产出为文本,使用 reviewPhaseCompletion
229
- // code/test/review 阶段:优先用 reviewForPhase(有代码内容),否则降级到 reviewPhaseCompletion
230
- const phaseTasks = pipeline.tasks.filter(t => t.phase === phase);
231
- const phaseContext = phaseTasks
232
- .map(t => `- ${t.title}: ${t.output || '(进行中)'}`)
233
- .join('\n');
234
- // 活动阈值强制完成时,任务 output 为空,质量门禁无法评审产出物。
235
- // 此时跳过 reviewPhaseCompletion,视为通过(活动阈值本身已作为充分性判断)。
236
- const hasRealOutput = phaseTasks.some(t => t.output && t.output.trim().length > 0);
237
- const skipGate = !hasRealOutput && (phase === 'analyze' || phase === 'design');
238
- if (skipGate) {
239
- logger.info(`[Pipeline] ${PHASE_LABELS[phase]}阶段任务均无产出物(活动阈值强制完成),跳过质量门禁,直接推进`);
240
- this.phaseFixAttempts.delete(fixKey);
241
- }
242
- let gateResult = skipGate ? null : (phase === 'analyze' || phase === 'design')
243
- ? await qualityGate.reviewPhaseCompletion(phase, pipeline.requirement, phaseContext)
244
- : await qualityGate.reviewForPhase(event, phase);
245
- // reviewForPhase 返回 null 表示事件不适用(非 Write/Edit),降级到 reviewPhaseCompletion
246
- if (gateResult === null && phase !== 'analyze' && phase !== 'design') {
247
- gateResult = await qualityGate.reviewPhaseCompletion(phase, pipeline.requirement, phaseContext);
248
- }
249
- if (gateResult && gateResult.level === 'fail') {
250
- const newAttempts = fixAttempts + 1;
251
- this.phaseFixAttempts.set(fixKey, newAttempts);
252
- const failIssues = gateResult.checks
253
- .filter(c => c.level === 'fail')
254
- .map(c => `[${c.category}] ${c.message}${c.suggestion ? `\n 修复建议:${c.suggestion}` : ''}`);
255
- if (newAttempts >= PhaseManager.MAX_FIX_ATTEMPTS) {
256
- // 达到最大重试次数,强制放行并记录警告——避免 Pipeline 永久卡死产生垃圾数据
257
- logger.warn(`[Pipeline] 质量门禁阶段 ${PHASE_LABELS[phase]} 已达最大修复次数(${PhaseManager.MAX_FIX_ATTEMPTS}),强制放行,记录警告`);
258
- this.phaseFixAttempts.delete(fixKey);
259
- // return,继续执行后续阶段推进逻辑,让 Pipeline 正常前进
260
- }
261
- else {
262
- logger.warn(`[Pipeline] 质量门禁阻止推进 ${PHASE_LABELS[phase]}(第 ${newAttempts}/${PhaseManager.MAX_FIX_ATTEMPTS} 次):${gateResult.summary}`);
263
- const completedTasks = pipeline.tasks
264
- .filter(t => t.phase === phase && t.status === 'completed')
265
- .map(t => `- ${t.title}`)
266
- .join('\n') || '(无)';
267
- // 按严重程度排序:security > spec > quality > test > performance
268
- const priorityOrder = ['security', 'spec', 'quality', 'test', 'performance'];
269
- const sortedIssues = [...failIssues].sort((a, b) => {
270
- const aIdx = priorityOrder.findIndex(p => a.includes(`[${p}]`));
271
- const bIdx = priorityOrder.findIndex(p => b.includes(`[${p}]`));
272
- return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
273
- });
274
- const systemMessage = `[Forge 质量门禁] ❌ ${PHASE_LABELS[phase]}阶段检查未通过(${newAttempts}/${PhaseManager.MAX_FIX_ATTEMPTS})
275
-
276
- ## 必须修复(按优先级)
277
-
278
- ${sortedIssues.join('\n\n')}
279
-
280
- ## 快速修复步骤
281
- 1. 优先修复 [security] 问题(安全漏洞)
282
- 2. 其次修复 [spec] 问题(需求不符)
283
- 3. 最后处理 [quality]/[test] 问题
284
- 4. 修复后继续工作,门禁将自动重新检查
285
- ${newAttempts >= PhaseManager.MAX_FIX_ATTEMPTS - 1 ? '\n⚠️ 最后机会:下次仍不通过将强制放行。' : ''}`;
286
- return {
287
- advanced: false,
288
- qualityBlocked: true,
289
- qualityIssues: failIssues,
290
- systemMessage,
291
- qualityLevel: 'fail',
292
- fixAttempts: newAttempts,
293
- };
294
- }
295
- }
296
- else {
297
- // pass 或 warn:清理修复计数,允许阶段推进
298
- // warn 不阻断,仅记录日志
299
- qualityLevel = gateResult?.level ?? 'pass';
300
- if (fixAttempts > 0) {
301
- logger.info(`[Pipeline] 质量门禁 ${PHASE_LABELS[phase]} 通过(经过 ${fixAttempts} 次修复)`);
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 (gateResult?.level === 'warn') {
304
- logger.warn(`[Pipeline] 质量门禁 ${PHASE_LABELS[phase]} 有警告但允许推进:${gateResult.summary}`);
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
- this.store.updatePhase(pipeline.id, newPh);
326
- // totalPhaseEventCounts 保留累积记录,供复盘分析使用
327
- this.phaseToolsSeen.delete(`${pipeline.id}:${phase}`);
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
- return { advanced: false };
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, toolsSeen) {
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
- return true;
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
- return true;
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
- // 持久化到 toolsSeenSet(L176 会自动写回 phaseToolsSeen),跨事件追踪
492
+ // 持久化标记(trackToolUsage 已更新 phaseToolsSeen
431
493
  if (hasTestFile)
432
- toolsSeen.add('test:file');
494
+ toolsSeenSet.add('test:file');
433
495
  if (hasTestRun)
434
- toolsSeen.add('test:run');
435
- return toolsSeen.has('test:file') && toolsSeen.has('test:run');
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 中把关)