@winspan/claude-forge 0.5.2 → 0.6.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.
Files changed (182) hide show
  1. package/README.md +3 -3
  2. package/dist/ai-gateway/index.d.ts +26 -0
  3. package/dist/ai-gateway/index.d.ts.map +1 -0
  4. package/dist/ai-gateway/index.js +67 -0
  5. package/dist/ai-gateway/index.js.map +1 -0
  6. package/dist/ai-gateway/model-selector.d.ts +6 -0
  7. package/dist/ai-gateway/model-selector.d.ts.map +1 -0
  8. package/dist/ai-gateway/model-selector.js +36 -0
  9. package/dist/ai-gateway/model-selector.js.map +1 -0
  10. package/dist/ai-gateway/rate-limiter.d.ts +20 -0
  11. package/dist/ai-gateway/rate-limiter.d.ts.map +1 -0
  12. package/dist/ai-gateway/rate-limiter.js +45 -0
  13. package/dist/ai-gateway/rate-limiter.js.map +1 -0
  14. package/dist/ai-gateway/response-cache.d.ts +17 -0
  15. package/dist/ai-gateway/response-cache.d.ts.map +1 -0
  16. package/dist/ai-gateway/response-cache.js +44 -0
  17. package/dist/ai-gateway/response-cache.js.map +1 -0
  18. package/dist/ai-provider/types.d.ts +2 -0
  19. package/dist/ai-provider/types.d.ts.map +1 -1
  20. package/dist/ai-provider/types.js.map +1 -1
  21. package/dist/daemon/handlers/context-builder.d.ts +55 -0
  22. package/dist/daemon/handlers/context-builder.d.ts.map +1 -0
  23. package/dist/daemon/handlers/context-builder.js +429 -0
  24. package/dist/daemon/handlers/context-builder.js.map +1 -0
  25. package/dist/daemon/handlers/orchestration-context.d.ts +37 -0
  26. package/dist/daemon/handlers/orchestration-context.d.ts.map +1 -0
  27. package/dist/daemon/handlers/orchestration-context.js +2 -0
  28. package/dist/daemon/handlers/orchestration-context.js.map +1 -0
  29. package/dist/daemon/handlers/session-cleanup.js +5 -5
  30. package/dist/daemon/handlers/session-cleanup.js.map +1 -1
  31. package/dist/daemon/handlers/stages/01-failure-signal.d.ts +8 -0
  32. package/dist/daemon/handlers/stages/01-failure-signal.d.ts.map +1 -0
  33. package/dist/daemon/handlers/stages/01-failure-signal.js +39 -0
  34. package/dist/daemon/handlers/stages/01-failure-signal.js.map +1 -0
  35. package/dist/daemon/handlers/stages/02-active-intervention.d.ts +8 -0
  36. package/dist/daemon/handlers/stages/02-active-intervention.d.ts.map +1 -0
  37. package/dist/daemon/handlers/stages/02-active-intervention.js +27 -0
  38. package/dist/daemon/handlers/stages/02-active-intervention.js.map +1 -0
  39. package/dist/daemon/handlers/stages/03-init-prompt.d.ts +8 -0
  40. package/dist/daemon/handlers/stages/03-init-prompt.d.ts.map +1 -0
  41. package/dist/daemon/handlers/stages/03-init-prompt.js +24 -0
  42. package/dist/daemon/handlers/stages/03-init-prompt.js.map +1 -0
  43. package/dist/daemon/handlers/stages/04-skill-suggestions.d.ts +8 -0
  44. package/dist/daemon/handlers/stages/04-skill-suggestions.d.ts.map +1 -0
  45. package/dist/daemon/handlers/stages/04-skill-suggestions.js +23 -0
  46. package/dist/daemon/handlers/stages/04-skill-suggestions.js.map +1 -0
  47. package/dist/daemon/handlers/stages/05-conv-config.d.ts +10 -0
  48. package/dist/daemon/handlers/stages/05-conv-config.d.ts.map +1 -0
  49. package/dist/daemon/handlers/stages/05-conv-config.js +23 -0
  50. package/dist/daemon/handlers/stages/05-conv-config.js.map +1 -0
  51. package/dist/daemon/handlers/stages/06-engine-check.d.ts +8 -0
  52. package/dist/daemon/handlers/stages/06-engine-check.d.ts.map +1 -0
  53. package/dist/daemon/handlers/stages/06-engine-check.js +13 -0
  54. package/dist/daemon/handlers/stages/06-engine-check.js.map +1 -0
  55. package/dist/daemon/handlers/stages/07-pipeline-reply.d.ts +9 -0
  56. package/dist/daemon/handlers/stages/07-pipeline-reply.d.ts.map +1 -0
  57. package/dist/daemon/handlers/stages/07-pipeline-reply.js +44 -0
  58. package/dist/daemon/handlers/stages/07-pipeline-reply.js.map +1 -0
  59. package/dist/daemon/handlers/stages/08-esc-interrupt.d.ts +9 -0
  60. package/dist/daemon/handlers/stages/08-esc-interrupt.d.ts.map +1 -0
  61. package/dist/daemon/handlers/stages/08-esc-interrupt.js +52 -0
  62. package/dist/daemon/handlers/stages/08-esc-interrupt.js.map +1 -0
  63. package/dist/daemon/handlers/stages/09-pipeline-active.d.ts +8 -0
  64. package/dist/daemon/handlers/stages/09-pipeline-active.d.ts.map +1 -0
  65. package/dist/daemon/handlers/stages/09-pipeline-active.js +20 -0
  66. package/dist/daemon/handlers/stages/09-pipeline-active.js.map +1 -0
  67. package/dist/daemon/handlers/stages/10-cooldown.d.ts +9 -0
  68. package/dist/daemon/handlers/stages/10-cooldown.d.ts.map +1 -0
  69. package/dist/daemon/handlers/stages/10-cooldown.js +44 -0
  70. package/dist/daemon/handlers/stages/10-cooldown.js.map +1 -0
  71. package/dist/daemon/handlers/stages/11-intent-analysis.d.ts +8 -0
  72. package/dist/daemon/handlers/stages/11-intent-analysis.d.ts.map +1 -0
  73. package/dist/daemon/handlers/stages/11-intent-analysis.js +38 -0
  74. package/dist/daemon/handlers/stages/11-intent-analysis.js.map +1 -0
  75. package/dist/daemon/handlers/stages/12-strategy-advice.d.ts +8 -0
  76. package/dist/daemon/handlers/stages/12-strategy-advice.d.ts.map +1 -0
  77. package/dist/daemon/handlers/stages/12-strategy-advice.js +20 -0
  78. package/dist/daemon/handlers/stages/12-strategy-advice.js.map +1 -0
  79. package/dist/daemon/handlers/stages/13-template-route.d.ts +8 -0
  80. package/dist/daemon/handlers/stages/13-template-route.d.ts.map +1 -0
  81. package/dist/daemon/handlers/stages/13-template-route.js +49 -0
  82. package/dist/daemon/handlers/stages/13-template-route.js.map +1 -0
  83. package/dist/daemon/handlers/stages/14-plan-resume.d.ts +8 -0
  84. package/dist/daemon/handlers/stages/14-plan-resume.d.ts.map +1 -0
  85. package/dist/daemon/handlers/stages/14-plan-resume.js +51 -0
  86. package/dist/daemon/handlers/stages/14-plan-resume.js.map +1 -0
  87. package/dist/daemon/handlers/stages/15-plan-enforcement.d.ts +8 -0
  88. package/dist/daemon/handlers/stages/15-plan-enforcement.d.ts.map +1 -0
  89. package/dist/daemon/handlers/stages/15-plan-enforcement.js +41 -0
  90. package/dist/daemon/handlers/stages/15-plan-enforcement.js.map +1 -0
  91. package/dist/daemon/handlers/stages/16-intervention-level.d.ts +8 -0
  92. package/dist/daemon/handlers/stages/16-intervention-level.d.ts.map +1 -0
  93. package/dist/daemon/handlers/stages/16-intervention-level.js +24 -0
  94. package/dist/daemon/handlers/stages/16-intervention-level.js.map +1 -0
  95. package/dist/daemon/handlers/stages/17-simple-task.d.ts +8 -0
  96. package/dist/daemon/handlers/stages/17-simple-task.d.ts.map +1 -0
  97. package/dist/daemon/handlers/stages/17-simple-task.js +47 -0
  98. package/dist/daemon/handlers/stages/17-simple-task.js.map +1 -0
  99. package/dist/daemon/handlers/stages/18-complex-task.d.ts +11 -0
  100. package/dist/daemon/handlers/stages/18-complex-task.d.ts.map +1 -0
  101. package/dist/daemon/handlers/stages/18-complex-task.js +84 -0
  102. package/dist/daemon/handlers/stages/18-complex-task.js.map +1 -0
  103. package/dist/daemon/handlers/stages/19-moderate-task.d.ts +8 -0
  104. package/dist/daemon/handlers/stages/19-moderate-task.d.ts.map +1 -0
  105. package/dist/daemon/handlers/stages/19-moderate-task.js +62 -0
  106. package/dist/daemon/handlers/stages/19-moderate-task.js.map +1 -0
  107. package/dist/daemon/handlers/stages/stage-interface.d.ts +24 -0
  108. package/dist/daemon/handlers/stages/stage-interface.d.ts.map +1 -0
  109. package/dist/daemon/handlers/stages/stage-interface.js +2 -0
  110. package/dist/daemon/handlers/stages/stage-interface.js.map +1 -0
  111. package/dist/daemon/handlers/stop-handler.js +2 -2
  112. package/dist/daemon/handlers/stop-handler.js.map +1 -1
  113. package/dist/daemon/handlers/user-prompt-handler.d.ts +1 -115
  114. package/dist/daemon/handlers/user-prompt-handler.d.ts.map +1 -1
  115. package/dist/daemon/handlers/user-prompt-handler.js +76 -1089
  116. package/dist/daemon/handlers/user-prompt-handler.js.map +1 -1
  117. package/dist/daemon/index.d.ts.map +1 -1
  118. package/dist/daemon/index.js +3 -1
  119. package/dist/daemon/index.js.map +1 -1
  120. package/dist/pipeline/artifact-generator.d.ts.map +1 -1
  121. package/dist/pipeline/artifact-generator.js +21 -0
  122. package/dist/pipeline/artifact-generator.js.map +1 -1
  123. package/dist/storage/repositories/api-usage-repository.d.ts +28 -0
  124. package/dist/storage/repositories/api-usage-repository.d.ts.map +1 -0
  125. package/dist/storage/repositories/api-usage-repository.js +59 -0
  126. package/dist/storage/repositories/api-usage-repository.js.map +1 -0
  127. package/dist/storage/repositories/base-repository.d.ts +6 -0
  128. package/dist/storage/repositories/base-repository.d.ts.map +1 -0
  129. package/dist/storage/repositories/base-repository.js +7 -0
  130. package/dist/storage/repositories/base-repository.js.map +1 -0
  131. package/dist/storage/repositories/distill-repository.d.ts +36 -0
  132. package/dist/storage/repositories/distill-repository.d.ts.map +1 -0
  133. package/dist/storage/repositories/distill-repository.js +85 -0
  134. package/dist/storage/repositories/distill-repository.js.map +1 -0
  135. package/dist/storage/repositories/event-repository.d.ts +14 -0
  136. package/dist/storage/repositories/event-repository.d.ts.map +1 -0
  137. package/dist/storage/repositories/event-repository.js +171 -0
  138. package/dist/storage/repositories/event-repository.js.map +1 -0
  139. package/dist/storage/repositories/failure-repository.d.ts +22 -0
  140. package/dist/storage/repositories/failure-repository.d.ts.map +1 -0
  141. package/dist/storage/repositories/failure-repository.js +26 -0
  142. package/dist/storage/repositories/failure-repository.js.map +1 -0
  143. package/dist/storage/repositories/intent-rule-repository.d.ts +20 -0
  144. package/dist/storage/repositories/intent-rule-repository.d.ts.map +1 -0
  145. package/dist/storage/repositories/intent-rule-repository.js +38 -0
  146. package/dist/storage/repositories/intent-rule-repository.js.map +1 -0
  147. package/dist/storage/repositories/knowledge-repository.d.ts +46 -0
  148. package/dist/storage/repositories/knowledge-repository.d.ts.map +1 -0
  149. package/dist/storage/repositories/knowledge-repository.js +84 -0
  150. package/dist/storage/repositories/knowledge-repository.js.map +1 -0
  151. package/dist/storage/repositories/latency-repository.d.ts +21 -0
  152. package/dist/storage/repositories/latency-repository.d.ts.map +1 -0
  153. package/dist/storage/repositories/latency-repository.js +42 -0
  154. package/dist/storage/repositories/latency-repository.js.map +1 -0
  155. package/dist/storage/repositories/maintenance-repository.d.ts +18 -0
  156. package/dist/storage/repositories/maintenance-repository.d.ts.map +1 -0
  157. package/dist/storage/repositories/maintenance-repository.js +61 -0
  158. package/dist/storage/repositories/maintenance-repository.js.map +1 -0
  159. package/dist/storage/repositories/satisfaction-repository.d.ts +21 -0
  160. package/dist/storage/repositories/satisfaction-repository.d.ts.map +1 -0
  161. package/dist/storage/repositories/satisfaction-repository.js +26 -0
  162. package/dist/storage/repositories/satisfaction-repository.js.map +1 -0
  163. package/dist/storage/repositories/session-repository.d.ts +16 -0
  164. package/dist/storage/repositories/session-repository.d.ts.map +1 -0
  165. package/dist/storage/repositories/session-repository.js +69 -0
  166. package/dist/storage/repositories/session-repository.js.map +1 -0
  167. package/dist/storage/repositories/task-repository.d.ts +82 -0
  168. package/dist/storage/repositories/task-repository.d.ts.map +1 -0
  169. package/dist/storage/repositories/task-repository.js +198 -0
  170. package/dist/storage/repositories/task-repository.js.map +1 -0
  171. package/dist/storage/sqlite.d.ts +25 -0
  172. package/dist/storage/sqlite.d.ts.map +1 -1
  173. package/dist/storage/sqlite.js +27 -0
  174. package/dist/storage/sqlite.js.map +1 -1
  175. package/dist/utils/claude-api.d.ts.map +1 -1
  176. package/dist/utils/claude-api.js +3 -1
  177. package/dist/utils/claude-api.js.map +1 -1
  178. package/dist/utils/logger.d.ts +7 -0
  179. package/dist/utils/logger.d.ts.map +1 -1
  180. package/dist/utils/logger.js +22 -0
  181. package/dist/utils/logger.js.map +1 -1
  182. package/package.json +1 -1
@@ -1,17 +1,26 @@
1
1
  import { BaseHookHandler } from './base-handler.js';
2
- import { ConversationalConfigHandler } from './conversational-config-handler.js';
3
- import { logger } from '../../utils/logger.js';
4
- import { ForgeFormatter } from '../../utils/formatter.js';
5
- import { parsePipelineChoice, PIPELINE_OPTIONS } from '../../pipeline/pipeline-options.js';
6
- import { readPlanFile, getPlanFilePath, buildSessionStartPlanContext, readActivePlanSummary } from '../../utils/plan-reader.js';
7
- import { ContextInjector, INJECT_PRIORITY } from '../context-injector.js';
8
- import { TreeIndexEngine } from '../../distill/tree-index.js';
2
+ import { logger, relPath } from '../../utils/logger.js';
9
3
  import { TemplateRegistry } from '../../pipeline/template-registry.js';
10
4
  import { TemplateRouter } from '../../pipeline/template-router.js';
11
- import fs from 'fs';
12
- import path from 'path';
13
- import os from 'os';
14
- import { isForgeManaged } from '../lifecycle.js';
5
+ import { FailureSignalStage } from './stages/01-failure-signal.js';
6
+ import { ActiveInterventionStage } from './stages/02-active-intervention.js';
7
+ import { InitPromptStage } from './stages/03-init-prompt.js';
8
+ import { SkillSuggestionsStage } from './stages/04-skill-suggestions.js';
9
+ import { ConversationalConfigStage } from './stages/05-conv-config.js';
10
+ import { EngineCheckStage } from './stages/06-engine-check.js';
11
+ import { PipelineReplyStage } from './stages/07-pipeline-reply.js';
12
+ import { EscInterruptStage } from './stages/08-esc-interrupt.js';
13
+ import { PipelineActiveStage } from './stages/09-pipeline-active.js';
14
+ import { CooldownStage } from './stages/10-cooldown.js';
15
+ import { IntentAnalysisStage } from './stages/11-intent-analysis.js';
16
+ import { StrategyAdviceStage } from './stages/12-strategy-advice.js';
17
+ import { TemplateRouteStage } from './stages/13-template-route.js';
18
+ import { PlanResumeStage } from './stages/14-plan-resume.js';
19
+ import { PlanEnforcementStage } from './stages/15-plan-enforcement.js';
20
+ import { InterventionLevelStage } from './stages/16-intervention-level.js';
21
+ import { SimpleTaskStage } from './stages/17-simple-task.js';
22
+ import { ComplexTaskStage } from './stages/18-complex-task.js';
23
+ import { ModerateTaskStage } from './stages/19-moderate-task.js';
15
24
  export class UserPromptHandler extends BaseHookHandler {
16
25
  /**
17
26
  * 上一轮是否向用户展示了 pipeline 建议(按 project_path 隔离)
@@ -30,16 +39,11 @@ export class UserPromptHandler extends BaseHookHandler {
30
39
  injectionCache = new Map();
31
40
  /** 已处理过负向重启检测的 session(key=sessionId),避免同 session 重复触发 */
32
41
  negativeRestartChecked = new Set();
33
- static ANALYSIS_COOLDOWN_MS = 800;
34
- /** Map 条目超过此时长(1 小时)后在下次写入时清理 */
35
- static MAX_STATE_AGE_MS = 60 * 60 * 1000;
36
- configHandler;
37
42
  /** Batch 4:统一编排模板注册表 + 路由器 */
38
43
  templateRegistry = null;
39
44
  templateRouter = null;
40
45
  constructor(ctx) {
41
46
  super(ctx);
42
- this.configHandler = new ConversationalConfigHandler(ctx);
43
47
  // 初始化模板路由器(仅在 sharedApi 可用时)
44
48
  if (ctx.sharedApi) {
45
49
  this.templateRegistry = new TemplateRegistry();
@@ -52,1085 +56,68 @@ export class UserPromptHandler extends BaseHookHandler {
52
56
  /** initPromptedSessions 条目的写入时间,用于 TTL 清理 */
53
57
  initPromptedAt = new Map();
54
58
  async handle(event) {
55
- const { intentEngine } = this.ctx;
56
59
  const userPrompt = event.tool_input?.user_prompt || '';
57
60
  if (!userPrompt)
58
61
  return;
59
- // 失败信号检测(异步写入,不阻塞主流程)
60
- if (this.ctx.retrospectiveEngine) {
61
- const toolCallCount = this.ctx.storage.queryEvents({
62
- session_id: event.session_id,
63
- project_path: event.project_path,
64
- hook_type: 'PostToolUse',
65
- }).length;
66
- const signal = this.ctx.retrospectiveEngine.detectFailureSignal(userPrompt, {
67
- preceding_tool_count: toolCallCount,
68
- current_phase: this.ctx.pipelineEngine?.getCurrentPhase(event.project_path) ?? undefined,
69
- timestamp: event.timestamp ?? new Date().toISOString(),
70
- });
71
- if (signal) {
72
- this.ctx.retrospectiveEngine.recordFailureSignal({
73
- ...signal,
74
- session_id: event.session_id,
75
- project_path: event.project_path,
76
- }).catch(err => logger.debug(`[Forge:任务复盘] 记录失败信号异常:${err}`));
77
- }
78
- // P4:负向重启回溯修正(仅在 session 内首条 UserPromptSubmit 时触发)
79
- if (!this.negativeRestartChecked.has(event.session_id)) {
80
- this.negativeRestartChecked.add(event.session_id);
81
- if (signal?.signal_type === 'dissatisfied') {
82
- this.applyNegativeRestartPenalty(event.project_path, event.session_id).catch(err => logger.debug(`[Forge:满意度] 负向重启回溯失败:${err}`));
83
- }
84
- }
85
- }
86
- // 主动干预:检测卡死循环(fix_attempt_count >= 3 时注入建议)
87
- if (this.ctx.retrospectiveEngine) {
88
- const sessionMetrics = this.ctx.storage.getSessionMetrics(event.session_id);
89
- const fixAttempts = Number(sessionMetrics?.['fix_attempt_count'] ?? 0);
90
- if (fixAttempts >= 3) {
91
- logger.info(`[Forge:主动干预] 检测到卡死循环(修复尝试次数=${fixAttempts}),注入干预建议`);
92
- return {
93
- allow: true,
94
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge 主动干预] 检测到您已进行 ${fixAttempts} 次修复尝试,建议:\n` +
95
- `1. 回退到上一个稳定状态(git stash 或 git checkout)\n` +
96
- `2. 重新分析根本原因,而非症状\n` +
97
- `3. 考虑使用 cf pipeline 启动结构化编排`),
98
- };
99
- }
100
- }
101
- // 0. 检测项目是否已初始化,未初始化时提示用户(每个 session 只提示一次)
102
- const sessionKey = `${event.session_id}:${event.project_path}`;
103
- if (!this.initPromptedSessions.has(sessionKey) && !isForgeManaged(event.project_path)) {
104
- logger.info(`项目未初始化,已注入配置引导提示`);
105
- this.initPromptedSessions.add(sessionKey);
106
- this.initPromptedAt.set(sessionKey, Date.now());
107
- return {
108
- allow: true,
109
- additionalContext: ForgeFormatter.wrapInQuote(`[Claude Forge 提示]\n当前项目(${event.project_path})尚未加入 Claude Forge 管理。\n请询问用户:"是否将当前项目加入 Claude Forge 管理?(将自动分析技术栈并生成项目规范)(是/否)"\n如果用户确认,请执行:\`\`\`bash\nmkdir -p "${event.project_path}/.claude-forge" && cd "${event.project_path}" && cf convention distill\n\`\`\`\n执行完成后告知用户项目已加入管理并生成了自定义规范。`),
110
- };
111
- }
112
- try {
113
- // 0a. 技能优化建议注入(每次 session 首条消息,读取后删除文件,仅注入一次)
114
- const skillSuggestions = this.readAndDeleteSkillSuggestions(event.project_path);
115
- if (skillSuggestions) {
116
- logger.info('[Forge:技能建议] 注入技能优化建议并删除建议文件');
117
- return {
118
- allow: true,
119
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge 技能优化建议]\n以下是上次 Forge 分析生成的技能改进建议,供参考:\n\n${skillSuggestions}`),
120
- };
121
- }
122
- // 0. 对话式配置意图检测(pattern-based,不依赖 AI,优先执行)
123
- const configMsg = await this.configHandler.handle(userPrompt, event.project_path);
124
- if (configMsg) {
125
- logger.info('检测到配置意图,已执行并注入确认消息');
126
- // 注入确认消息后继续让 Claude 处理原始消息
127
- return { allow: true, additionalContext: ForgeFormatter.wrapInQuote(configMsg) };
128
- }
129
- if (!intentEngine) {
130
- logger.info('AI 引擎不可用,跳过意图分析');
131
- return;
132
- }
133
- // 1. 检查是否是对 Pipeline 选择的回复(优先处理)
134
- const pipelineReply = await this.handlePipelineReply(userPrompt, event);
135
- if (pipelineReply !== undefined)
136
- return pipelineReply;
137
- // 1.2 检测 ESC 中断待确认状态
138
- const interruptedState = this.ctx.pipelineEngine?.getInterruptedState(event.project_path);
139
- if (interruptedState) {
140
- const interruptReply = this.handleInterruptReply(userPrompt, event, interruptedState);
141
- if (interruptReply !== undefined)
142
- return interruptReply;
143
- // 首次进入中断状态:注入消歧提示,等待用户回复
144
- const { phase, completedCount } = interruptedState;
145
- const PHASE_LABELS_MAP = {
146
- analyze: '需求分析', design: '架构设计', code: '编码实现', test: '测试', review: '代码审查',
147
- };
148
- const phaseLabel = PHASE_LABELS_MAP[phase] ?? phase;
149
- logger.info(`[Pipeline] 注入 ESC 中断消歧提示(阶段=${phaseLabel},已完成=${completedCount})`);
150
- return {
151
- allow: true,
152
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge Pipeline 中断恢复]\n检测到上次执行被中断(${phaseLabel}阶段,已完成 ${completedCount} 个任务)。\n\n请询问用户:\n"上次执行被中断,请选择处理方式:\nA. 继续 — 从中断处继续执行当前阶段\nB. 重做 — 重新执行当前阶段(清除本阶段进度)\nC. 取消 — 关闭当前 Pipeline,回到普通模式\n\n请输入 A、B 或 C:"`),
153
- };
154
- }
155
- // 1.5 Pipeline 活跃时短路:只注入规范,跳过意图分析和 profile/decisions 注入
156
- // Pipeline 运行中已通过 PreToolUse 注入阶段指令,UserPrompt 侧无需重复注入
157
- if (this.ctx.pipelineEngine?.hasActivePipeline(event.project_path)) {
158
- const conventionPrompt = this.ctx.conventionManager.getActivePrompt(event.project_path) ?? undefined;
159
- logger.info('[Forge:Pipeline] Pipeline 活跃,跳过意图分析和低优先级上下文注入');
160
- if (!conventionPrompt)
161
- return;
162
- return {
163
- allow: true,
164
- additionalContext: ForgeFormatter.wrapInQuote(conventionPrompt),
165
- };
166
- }
167
- // 2. 冷却检查(冷却期内仍注入规范上下文,避免 Claude 无任何约束)
168
- if (!this.checkCooldown(event.project_path)) {
169
- const conventionPrompt = this.ctx.conventionManager.getActivePrompt(event.project_path);
170
- if (conventionPrompt) {
171
- logger.info('[Forge:冷却期] 分析冷却中,仅注入规范上下文');
172
- return {
173
- allow: true,
174
- additionalContext: ForgeFormatter.wrapInQuote(conventionPrompt),
175
- };
176
- }
177
- return;
178
- }
179
- // 3. 意图分析(带会话上下文 + 活跃计划状态)
180
- const history = this.buildConversationHistory(event.session_id, event.project_path);
181
- // 前置读取活跃计划——意图分析必须感知到跨 session 的任务续接状态
182
- const activePlan = readActivePlanSummary(event.project_path) ?? undefined;
183
- // 预热:在 await analyze() 之前同步完成不依赖 analysis 的 IO,
184
- // 利用 AI 网络等待期间(2-3s)将结果写入 injectionCache,analyze 完成后直接命中缓存
185
- this.ctx.conventionManager.getActiveConventions(event.project_path);
186
- this.buildPlanStartContext(event.project_path);
187
- this.buildProfileContext(event.project_path);
188
- this.readDecisionsFile(event.project_path);
189
- // 构建用户上下文(从 ProfileManager + 历史满意度)
190
- const userContext = this.buildUserContext(event.project_path);
191
- const analysis = await this.ctx.latencyTracer.trace({
192
- session_id: event.session_id,
193
- project_path: event.project_path,
194
- trace_type: 'intent_analysis',
195
- engine_name: 'IntentEngine',
196
- }, () => intentEngine.analyze(userPrompt, event.project_path, undefined, history, isForgeManaged(event.project_path), activePlan, userContext));
197
- this.logAnalysis(analysis);
198
- if (analysis.searchKeywords?.length) {
199
- this.ctx.skillRegistry.setIntentKeywords(analysis.searchKeywords);
200
- }
201
- // 3.1 历史失败模式预防建议注入
202
- if (this.ctx.retrospectiveEngine) {
203
- const strategies = this.ctx.retrospectiveEngine.getStrategies(analysis.requirement || userPrompt);
204
- if (strategies) {
205
- logger.info('[Forge:策略建议] 注入历史失败模式预防建议');
206
- return {
207
- allow: true,
208
- additionalContext: ForgeFormatter.wrapInQuote(strategies),
209
- };
210
- }
211
- }
212
- // 3.5 模板路由(Batch 4:统一编排模板,优先于 Pattern 路由)
213
- const templateResponse = await this.handleTemplateRoute(userPrompt, analysis.complexity, event, analysis);
214
- if (templateResponse !== undefined)
215
- return templateResponse;
216
- // 3.7 计划续接路由:有活跃计划时,优先注入计划上下文,跳过 Plan Mode 强制和 Pipeline 建议
217
- if (activePlan && analysis.complexity !== 'simple') {
218
- const resumeResponse = this.handlePlanResume(activePlan, userPrompt, analysis.requirement, event.project_path);
219
- if (resumeResponse !== undefined)
220
- return resumeResponse;
221
- }
222
- // 3.8 计划强制检查(moderate/complex 任务有代码变更意图时,若无计划则强制先写计划)
223
- if (analysis.complexity !== 'simple') {
224
- const enforcement = this.checkPlanEnforcement(analysis.requirement, event.project_path);
225
- if (enforcement)
226
- return enforcement;
227
- }
228
- // 4. 干预级别
229
- const interventionLevel = this.ctx.profileManager.getInterventionLevel(analysis.complexity);
230
- logger.info(`用户偏好介入程度:${interventionLevel}(任务复杂度:${analysis.complexity})`);
231
- if (interventionLevel === 'silent') {
232
- // Forge 管理项目:即使静默模式,requiresPipeline=true 时仍需走编排(代码变更必须有产物)
233
- if (isForgeManaged(event.project_path) && analysis.requiresPipeline) {
234
- logger.info(`[Forge:Pipeline] 静默模式但 Forge 管理项目需要 Pipeline,继续编排`);
235
- }
236
- else {
237
- logger.info(`用户偏好静默模式,跳过编排`);
238
- return;
239
- }
240
- }
241
- // 5. 简单任务:轻量注入(只注入规范和画像,跳过执行卡片和 Pipeline)
242
- // Forge 管理项目:有代码改动意图时,提升为 moderate 并走 Pipeline 建议,确保产物生成
243
- if (analysis.complexity === 'simple') {
244
- const hasCodeChange = (analysis.estimatedFiles ?? 0) > 0;
245
- if (isForgeManaged(event.project_path) && hasCodeChange) {
246
- logger.info('[Forge:Pipeline] Forge 管理项目检测到代码改动,提升为 moderate 并走 Pipeline 建议');
247
- analysis.complexity = 'moderate';
248
- analysis.requiresPipeline = true;
249
- // 继续执行,不 return,走后续 Pipeline 建议逻辑
250
- }
251
- else {
252
- return this.buildLightweightResponse(analysis, event.project_path);
253
- }
254
- }
255
- // 6. 执行路径卡片(moderate/complex)
256
- const card = this.buildExecutionCard(analysis);
257
- // 7. 复杂任务 Pipeline 决策
258
- if (analysis.requiresPipeline) {
259
- const pipelineResponse = await this.handleComplexTask(analysis, card, event, interventionLevel);
260
- if (pipelineResponse !== undefined)
261
- return pipelineResponse;
262
- }
263
- // 8. 中等任务:知识搜索 + 指令注入
264
- const response = await this.buildSimpleResponse(analysis, card, event.project_path);
265
- return response;
266
- }
267
- catch (err) {
268
- logger.error(`意图分析失败:${err}`);
269
- }
270
- }
271
- // ── 私有方法 ──────────────────────────────────────────────────────────────
272
- /**
273
- * 处理用户对 Pipeline 选择的回复(A/B/C)。
274
- * 返回 HookResponse 表示已处理,返回 undefined 表示不是 pipeline 回复。
275
- */
276
- async handlePipelineReply(userPrompt, event) {
277
- const { pipelineEngine, profileManager } = this.ctx;
278
- if (!pipelineEngine || pipelineEngine.hasActivePipeline(event.project_path))
279
- return undefined;
280
- const choice = parsePipelineChoice(userPrompt);
281
- if (choice) {
282
- this.setSuggestionPending(event.project_path, false);
283
- logger.info(`用户选择了工作流方案:${choice}`);
284
- profileManager.recordBehavior({ type: 'accept', context: `pipeline:${choice}`, timestamp: new Date().toISOString() });
285
- return this.startPipelineFromChoice(choice, event);
286
- }
287
- if (this.suggestionPending.get(event.project_path)) {
288
- this.setSuggestionPending(event.project_path, false);
289
- logger.info('用户忽略了工作流建议');
290
- profileManager.recordBehavior({ type: 'skip', context: 'pipeline:ignored', timestamp: new Date().toISOString() });
291
- }
292
- return undefined;
293
- }
294
- /** 冷却检查,通过返回 true,并清理过期条目 */
295
- checkCooldown(projectPath) {
296
- const now = Date.now();
297
- this.evictStaleEntries(now);
298
- const lastAt = this.lastAnalysisAt.get(projectPath) ?? 0;
299
- if (now - lastAt < UserPromptHandler.ANALYSIS_COOLDOWN_MS) {
300
- logger.info(`分析冷却中,跳过(${projectPath})`);
301
- return false;
302
- }
303
- this.lastAnalysisAt.set(projectPath, now);
304
- return true;
305
- }
306
- /**
307
- * 构建会话上下文:最近的用户输入 + 工具调用摘要。
308
- * 帮助意图引擎理解当前对话在做什么,避免孤立判断单条输入。
309
- */
310
- buildConversationHistory(sessionId, projectPath) {
311
- try {
312
- const { storage } = this.ctx;
313
- // 查询最近 10 条事件(含 UserPromptSubmit 和工具调用)
314
- const recentEvents = storage.queryEvents({
315
- session_id: sessionId,
316
- project_path: projectPath,
317
- limit: 15,
318
- });
319
- if (recentEvents.length === 0)
320
- return undefined;
321
- const lines = [];
322
- // 提取最近的用户输入(最多 3 条,不含当前输入)
323
- const userPrompts = recentEvents
324
- .filter(e => e.hook_type === 'UserPromptSubmit' && e.tool_input)
325
- .slice(0, 4) // 多取一条因为第一条是当前输入
326
- .slice(1) // 去掉当前输入
327
- .reverse(); // 按时间正序
328
- if (userPrompts.length > 0) {
329
- lines.push('### 最近的用户指令');
330
- for (const e of userPrompts) {
331
- const prompt = e.tool_input?.user_prompt || '';
332
- if (prompt) {
333
- lines.push(`- ${prompt.substring(0, 150)}`);
334
- }
335
- }
336
- }
337
- // 提取最近的工具调用摘要(最多 5 条)
338
- const toolEvents = recentEvents
339
- .filter(e => (e.hook_type === 'PostToolUse' || e.hook_type === 'PreToolUse') && e.tool_name)
340
- .slice(0, 5)
341
- .reverse();
342
- if (toolEvents.length > 0) {
343
- lines.push('### 最近的工具调用');
344
- for (const e of toolEvents) {
345
- const inputStr = e.tool_input ? JSON.stringify(e.tool_input).substring(0, 100) : '';
346
- lines.push(`- ${e.tool_name}: ${inputStr}`);
347
- }
348
- }
349
- if (lines.length === 0)
350
- return undefined;
351
- return lines.join('\n');
352
- }
353
- catch {
354
- return undefined;
355
- }
356
- }
357
- /** 清理超过 MAX_STATE_AGE_MS 的 Map 条目,防止内存无限增长 */
358
- evictStaleEntries(now) {
359
- const cutoff = now - UserPromptHandler.MAX_STATE_AGE_MS;
360
- for (const [key, ts] of this.lastAnalysisAt) {
361
- if (ts < cutoff) {
362
- this.lastAnalysisAt.delete(key);
363
- this.suggestionPending.delete(key);
364
- this.injectionCache.delete(key);
365
- }
366
- }
367
- // 清理 initPromptedSessions 中超过 TTL 的条目
368
- for (const [key, ts] of this.initPromptedAt) {
369
- if (ts < cutoff) {
370
- this.initPromptedAt.delete(key);
371
- this.initPromptedSessions.delete(key);
372
- }
373
- }
374
- }
375
- setSuggestionPending(projectPath, value) {
376
- this.suggestionPending.set(projectPath, value);
377
- // 确保 lastAnalysisAt 有对应条目,使 evict 能清理 suggestionPending
378
- if (!this.lastAnalysisAt.has(projectPath)) {
379
- this.lastAnalysisAt.set(projectPath, Date.now());
380
- }
381
- }
382
- /**
383
- * 构建用户画像上下文。每个项目的 session 内只注入一次(首条消息),后续跳过。
384
- */
385
- buildProfileContext(projectPath) {
386
- const cache = this.injectionCache.get(projectPath);
387
- if (cache?.profileInjected)
388
- return undefined;
389
- const profile = this.ctx.profileManager.get();
390
- if (!profile.initialized)
391
- return undefined;
392
- // 标记已注入
393
- this.injectionCache.set(projectPath, {
394
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, planContextInjected: false, skillSuggestionsInjected: false }),
395
- profileInjected: true,
396
- });
397
- // 读取项目人设(优先级:.forge-modules.yaml > .claude-forge/persona.md)
398
- const persona = this.readProjectPersona(projectPath);
399
- logger.info(`注入用户画像:角色=${profile.role} 经验=${profile.experience_level} 风格=${profile.work_style}${persona ? ' 人设=已加载' : ''}`);
400
- return ForgeFormatter.formatProfileContext(profile, persona);
401
- }
402
- /** 读取项目人设文本,无则返回 undefined */
403
- readProjectPersona(projectPath) {
404
- try {
405
- // 1. .forge-modules.yaml persona 字段
406
- const modulesYaml = path.join(projectPath, '.forge-modules.yaml');
407
- if (fs.existsSync(modulesYaml)) {
408
- const raw = fs.readFileSync(modulesYaml, 'utf-8');
409
- const match = raw.match(/^persona:\s*["']?(.+?)["']?\s*$/m);
410
- if (match?.[1]?.trim())
411
- return match[1].trim();
412
- }
413
- // 2. .claude-forge/persona.md
414
- const personaFile = path.join(projectPath, '.claude-forge', 'persona.md');
415
- if (fs.existsSync(personaFile)) {
416
- const stored = fs.readFileSync(personaFile, 'utf-8').trim();
417
- if (stored)
418
- return stored;
419
- }
420
- }
421
- catch {
422
- // 读取失败静默忽略
423
- }
424
- return undefined;
425
- }
426
- /**
427
- * 构建计划进度上下文。每个项目的 session 内只注入一次(首条消息),后续跳过。
428
- * 优先级 -2,早于画像(-1)展示。
429
- */
430
- buildPlanStartContext(projectPath) {
431
- const cache = this.injectionCache.get(projectPath);
432
- if (cache?.planContextInjected)
433
- return undefined;
434
- this.injectionCache.set(projectPath, {
435
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, skillSuggestionsInjected: false }),
436
- planContextInjected: true,
437
- });
438
- const ctx = buildSessionStartPlanContext(projectPath);
439
- if (ctx) {
440
- logger.info(`注入计划上下文:${projectPath}`);
441
- }
442
- return ctx ?? undefined;
443
- }
444
- /**
445
- * 读取技能优化建议文件(~/.claude-forge/skill-suggestions.md),读取后立即删除。
446
- * 每个项目的 session 内只读一次(InjectionCache 标记),防止重复注入。
447
- */
448
- readAndDeleteSkillSuggestions(projectPath) {
449
- const cache = this.injectionCache.get(projectPath);
450
- if (cache?.skillSuggestionsInjected)
451
- return undefined;
452
- this.injectionCache.set(projectPath, {
453
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, planContextInjected: false, skillSuggestionsInjected: false }),
454
- skillSuggestionsInjected: true,
455
- });
456
- try {
457
- const suggestionsFile = path.join(os.homedir(), '.claude-forge', 'skill-suggestions.md');
458
- if (!fs.existsSync(suggestionsFile))
459
- return undefined;
460
- const content = fs.readFileSync(suggestionsFile, 'utf-8').trim();
461
- fs.unlinkSync(suggestionsFile);
462
- if (!content)
463
- return undefined;
464
- // 去掉文件头行("# 技能优化建议\n\n"),只返回建议正文
465
- return content.replace(/^#\s*.+\n+/, '').trim() || undefined;
466
- }
467
- catch {
468
- return undefined;
469
- }
470
- }
471
- /** 判断用户输入是否含有代码变更意图 */
472
- isCodeChangeIntent(requirement) {
473
- return /implement|add|fix|refactor|create|write|edit|update|build|删除|修复|实现|新增|添加|编写|创建|重构|开发/i.test(requirement);
474
- }
475
- /**
476
- * 处理 ESC 中断后用户的 A/B/C 选择回复。
477
- * 返回 HookResponse 表示已处理;返回 undefined 表示不是选择回复(首次进入,需注入提示)。
478
- */
479
- handleInterruptReply(userPrompt, event, interruptedState) {
480
- const choice = userPrompt.trim().toUpperCase();
481
- const pipelineEngine = this.ctx.pipelineEngine;
482
- if (!pipelineEngine)
483
- return undefined;
484
- if (choice === 'A' || /^继续/.test(userPrompt)) {
485
- pipelineEngine.resumeAfterInterrupt(event.project_path);
486
- logger.info(`[Pipeline] 用户选择继续,恢复 ${interruptedState.phase} 阶段`);
487
- return {
488
- allow: true,
489
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge Pipeline] 已恢复执行。继续 ${interruptedState.phase} 阶段,请从上次中断处继续工作。`),
490
- };
491
- }
492
- if (choice === 'B' || /^重做/.test(userPrompt)) {
493
- // 重做:取消当前 pipeline,让用户重新触发
494
- pipelineEngine.cancelAfterInterrupt(event.project_path);
495
- logger.info(`[Pipeline] 用户选择重做,关闭 Pipeline`);
496
- return {
497
- allow: true,
498
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge Pipeline] 已关闭当前 Pipeline。请重新描述需求,系统将重新启动编排流程。`),
499
- };
500
- }
501
- if (choice === 'C' || /^取消/.test(userPrompt)) {
502
- pipelineEngine.cancelAfterInterrupt(event.project_path);
503
- logger.info(`[Pipeline] 用户选择取消,关闭 Pipeline`);
504
- return {
505
- allow: true,
506
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge Pipeline] Pipeline 已取消,回到普通模式。`),
507
- };
508
- }
509
- // 不是 A/B/C 回复,说明是首次进入中断状态,返回 undefined 让外层注入提示
510
- return undefined;
511
- }
512
- /**
513
- * 计划续接路由:有活跃计划时,判断用户输入是"续接"还是"新任务"。
514
- *
515
- * - 续接意图("继续"/"下一步"/任务关键词匹配)→ 注入计划上下文,让 Claude 直接执行
516
- * - 新任务意图 → 注入冲突提示,让用户选择覆盖还是挂起
517
- * - 无法判断 → 返回 undefined,交给后续逻辑处理
518
- */
519
- handlePlanResume(activePlan, userPrompt, requirement, projectPath) {
520
- const lower = userPrompt.toLowerCase().trim();
521
- // 明确续接信号:通用续接词
522
- const resumePatterns = /^(继续|下一步|接着|continue|next|开始|go ahead|执行)/i;
523
- const isExplicitResume = resumePatterns.test(lower);
524
- // 任务关键词匹配:用户输入的任务名与计划中的待处理任务高度重叠
525
- const allPlanTasks = [...activePlan.inProgress, ...activePlan.pending];
526
- const isTaskMatch = allPlanTasks.some(task => task.length > 4 && lower.includes(task.toLowerCase().substring(0, Math.min(task.length, 10))));
527
- const isResume = isExplicitResume || isTaskMatch;
528
- if (isResume) {
529
- // 续接路径:注入完整计划上下文,Claude 直接执行
530
- logger.info(`[Forge:计划续接] 检测到续接意图,注入计划上下文(${activePlan.relPath})`);
531
- const inProgressList = activePlan.inProgress.length > 0
532
- ? `**进行中**:\n${activePlan.inProgress.map(t => ` - [~] ${t}`).join('\n')}`
533
- : '';
534
- const pendingList = activePlan.pending.length > 0
535
- ? `**待处理**:\n${activePlan.pending.map(t => ` - [ ] ${t}`).join('\n')}`
536
- : '';
537
- const context = [
538
- `[Forge 计划续接] 当前活跃计划:${activePlan.title}(${activePlan.relPath})`,
539
- `已完成 ${activePlan.doneCount} 个任务。`,
540
- inProgressList,
541
- pendingList,
542
- `\n请从上述任务继续执行。完成每个任务后,将计划文件中对应任务的 \`[ ]\` 或 \`[~]\` 改为 \`[x]\`。`,
543
- ].filter(Boolean).join('\n');
544
- return {
545
- allow: true,
546
- additionalContext: ForgeFormatter.wrapInQuote(context),
547
- };
548
- }
549
- // 新任务意图:检查是否与计划明显无关(有明确的新需求动词)
550
- const newTaskSignal = this.isCodeChangeIntent(requirement);
551
- if (newTaskSignal) {
552
- // 注入冲突提示,让用户决策
553
- logger.info(`[Forge:计划续接] 检测到新任务意图,注入冲突提示`);
554
- const inProgressSummary = activePlan.inProgress.length > 0
555
- ? activePlan.inProgress[0]
556
- : activePlan.pending[0] ?? '未知';
557
- return {
558
- allow: true,
559
- additionalContext: ForgeFormatter.wrapInQuote(`[Forge 计划冲突提示] 项目当前有未完成的计划:「${activePlan.title}」\n` +
560
- `当前进行中任务:${inProgressSummary}\n\n` +
561
- `您的新请求「${requirement}」与现有计划可能冲突。\n\n` +
562
- `请选择:\n` +
563
- `A. 继续现有计划(忽略新请求,专注完成当前任务)\n` +
564
- `B. 处理新请求(暂停现有计划,处理完后再回来)\n` +
565
- `C. 替换计划(放弃现有计划,为新请求重新制定计划)`),
566
- };
567
- }
568
- return undefined;
569
- }
570
- /**
571
- * 计划强制检查:检测到代码变更意图但今日无活跃计划时,自动启动 plan mode。
572
- */
573
- checkPlanEnforcement(requirement, projectPath) {
574
- if (!this.isCodeChangeIntent(requirement))
575
- return undefined;
576
- const planFile = readPlanFile(projectPath);
577
- if (planFile?.hasInProgress || planFile?.hasIncomplete)
578
- return undefined;
579
- const planPath = getPlanFilePath(projectPath);
580
- const msg = `[Forge 计划优先] 检测到代码变更意图,但项目 docs/ 下没有含待处理任务的计划文件。
581
-
582
- 正在自动启动 Plan Mode,请先规划任务步骤...`;
583
- logger.info(`[Forge:计划] 自动启动 Plan Mode,产物将同步至:${planPath}`);
584
- return {
585
- allow: true,
586
- systemMessage: `You are now in PLAN MODE. The user wants to: "${requirement}"
587
-
588
- Before implementing, you must:
589
- 1. Use EnterPlanMode tool to start planning
590
- 2. Create a detailed implementation plan with clear task breakdown
591
- 3. Break down the work into discrete tasks with clear acceptance criteria
592
- 4. Use ExitPlanMode when the plan is complete
593
- 5. IMMEDIATELY after ExitPlanMode is approved, use the Write tool to save the plan to: ${planPath}
594
- - Create parent directories as needed
595
- - The file content must be the full plan document (same content as the plan)
596
- - This is MANDATORY — the plan MUST land in the project docs directory, not only in ~/.claude/plans/
597
-
598
- Do NOT write any code until the plan is approved and saved to ${planPath}. Start by calling EnterPlanMode.`,
599
- additionalContext: msg
62
+ const ctx = {
63
+ event,
64
+ userPrompt,
65
+ handlerCtx: this.ctx,
66
+ suggestionPending: this.suggestionPending,
67
+ lastAnalysisAt: this.lastAnalysisAt,
68
+ injectionCache: this.injectionCache,
69
+ negativeRestartChecked: this.negativeRestartChecked,
70
+ initPromptedSessions: this.initPromptedSessions,
71
+ initPromptedAt: this.initPromptedAt,
72
+ templateRegistry: this.templateRegistry,
73
+ templateRouter: this.templateRouter,
600
74
  };
601
- }
602
- logAnalysis(analysis) {
603
- const pipelineStr = analysis.requiresPipeline ? '需要多步骤编排' : '直接执行';
604
- logger.info(`意图分析:${analysis.complexity} | ${pipelineStr} | ` +
605
- `预估文件=${analysis.estimatedFiles} | 需求="${analysis.requirement}"`);
606
- logger.info(`分析理由:${analysis.reasoning}`);
607
- }
608
- /**
609
- * 模板路由(Batch 4):若命中 PipelineTemplate,直接启动模板驱动的 Pipeline。
610
- * 未命中或模板路由不可用时返回 undefined,继续走 Pattern 路由。
611
- */
612
- async handleTemplateRoute(userPrompt, complexity, event, analysis) {
613
- const { pipelineEngine } = this.ctx;
614
- if (!this.templateRegistry || !this.templateRouter || !pipelineEngine)
615
- return undefined;
616
- // Pipeline 活跃时跳过模板路由
617
- if (pipelineEngine.hasActivePipeline(event.project_path))
618
- return undefined;
619
- // simple 任务不走模板路由
620
- if (complexity === 'simple')
621
- return undefined;
622
- try {
623
- const templates = this.templateRegistry.getTemplates(event.project_path);
624
- if (templates.length === 0)
625
- return undefined;
626
- this.templateRouter.setTemplates(templates);
627
- const result = await this.ctx.latencyTracer.trace({
628
- session_id: event.session_id,
629
- project_path: event.project_path,
630
- trace_type: 'pattern_route',
631
- engine_name: 'TemplateRouter',
632
- }, () => this.templateRouter.route(userPrompt, complexity));
633
- if (!result.template || result.confidence < 0.6)
634
- return undefined;
635
- logger.info(`[模板路由] 命中:${result.template.id}(方式=${result.method}, 置信度=${result.confidence.toFixed(2)})`);
636
- const pipeline = await pipelineEngine.startPipelineFromTemplate(analysis.requirement, event.project_path, event.session_id, result.template);
637
- if (this.ctx.qualityGate)
638
- this.ctx.qualityGate.setRequirement(analysis.requirement);
639
- const card = this.buildExecutionCard(analysis);
640
- const msg = ForgeFormatter.formatModuleNotification('pipeline', `已启动模板编排(${result.template.name}),共 ${pipeline.tasks.length} 个任务\n阶段:${result.template.phases.map(p => p.name).join(' → ')}`);
641
- return { allow: true, systemMessage: card, additionalContext: ForgeFormatter.wrapInQuote(msg) };
642
- }
643
- catch (err) {
644
- logger.warn(`[模板路由] 路由失败,降级到 Pattern 路由:${err}`);
645
- return undefined;
646
- }
647
- }
648
- buildExecutionCard(analysis) {
649
- return ForgeFormatter.formatExecutionCard({
650
- intent: analysis.requirement,
651
- complexity: analysis.complexity,
652
- path: analysis.requiresPipeline ? '编排流程' : '直接执行',
653
- plan: analysis.suggestedPhases.length > 0 ? analysis.suggestedPhases : ['分析', '执行', '验证'],
654
- estimatedTime: analysis.requiresPipeline ? '30-45分钟' : undefined,
655
- clarifyQuestions: analysis.clarifyQuestions,
656
- });
657
- }
658
- /** 处理复杂任务的 Pipeline 决策,返回 HookResponse 或 undefined(继续走简单路径) */
659
- async handleComplexTask(analysis, card, event, interventionLevel) {
660
- const { pipelineEngine, qualityGate, config } = this.ctx;
661
- if (!pipelineEngine || !config.autopilot.auto_start_pipeline)
662
- return undefined;
663
- if (pipelineEngine.hasActivePipeline(event.project_path)) {
664
- logger.info('已有活跃的编排流程');
665
- return undefined;
666
- }
667
- const confirmMode = config.autopilot.pipeline_confirm_mode;
668
- if (interventionLevel === 'auto' || confirmMode === 'auto') {
669
- return this.autoStartPipeline(analysis, card, event, qualityGate);
670
- }
671
- if (confirmMode === 'prompt') {
672
- return this.promptPipelineChoice(analysis, card, event);
673
- }
674
- // preview 模式:仅展示,不启动
675
- logger.info('预览模式,不启动编排流程');
676
- return undefined;
677
- }
678
- async autoStartPipeline(analysis, card, event, qualityGate) {
679
- const pipeline = await this.ctx.pipelineEngine.startPipeline(analysis.requirement, event.project_path, event.session_id);
680
- if (qualityGate)
681
- qualityGate.setRequirement(analysis.requirement);
682
- logger.info(`[Forge:Pipeline] 自动启动:id=${pipeline.id} | tasks=${pipeline.tasks.length}`);
683
- const autoMsg = ForgeFormatter.formatModuleNotification('pipeline', `已自动启动完整编排,共 ${pipeline.tasks.length} 个任务\n阶段:${analysis.suggestedPhases.join(' → ')}`);
684
- // 加载项目规范并记录
685
- const activeConventions = this.ctx.conventionManager.getActiveConventions(event.project_path);
686
- const conventionPrompt = this.ctx.conventionManager.getActivePrompt(event.project_path) ?? undefined;
687
- if (activeConventions.length > 0) {
688
- logger.info(`已加载项目规范:${ForgeFormatter.formatConventionSummary(activeConventions)}`);
689
- }
690
- const decisionsCtx = await this.readDecisionsContext(analysis.requirement, event.project_path);
691
- const context = ForgeFormatter.wrapInQuote(ContextInjector.build({ content: this.buildPlanStartContext(event.project_path), priority: INJECT_PRIORITY.PLAN, topic: 'plan' }, { content: this.buildProfileContext(event.project_path), priority: INJECT_PRIORITY.PROFILE, topic: 'profile' }, { content: conventionPrompt, priority: INJECT_PRIORITY.CONVENTION, topic: 'convention' }, { content: autoMsg, priority: INJECT_PRIORITY.PIPELINE, topic: 'pipeline' }, { content: decisionsCtx, priority: INJECT_PRIORITY.DECISIONS, topic: 'decisions' }));
692
- if (analysis.clarifyQuestions?.length) {
693
- logger.info(`[Forge:Pipeline] 自动启动 + 注入澄清问题 ${analysis.clarifyQuestions.length} 个`);
694
- const clarify = ForgeFormatter.formatClarifyRequest(analysis.clarifyQuestions);
695
- return { allow: true, systemMessage: card, additionalContext: ForgeFormatter.mergeContexts(context, clarify) };
696
- }
697
- // 汇总注入内容
698
- const pipelineInjected = ['Pipeline指令'];
699
- if (conventionPrompt)
700
- pipelineInjected.push(`规范(${activeConventions.map(c => c.id).join(',')})`);
701
- logger.info(`自动启动多步骤编排 | 任务数=${pipeline.tasks.length} | 注入=[${pipelineInjected.join(', ')}]`);
702
- return { allow: true, systemMessage: card, additionalContext: context };
703
- }
704
- promptPipelineChoice(analysis, card, event) {
705
- const options = ForgeFormatter.formatPipelineOptions(analysis.suggestedPhases, analysis.requirement);
706
- this.setSuggestionPending(event.project_path, true);
707
- if (analysis.clarifyQuestions?.length) {
708
- const clarify = ForgeFormatter.formatClarifyRequest(analysis.clarifyQuestions);
709
- logger.info('向用户展示澄清问题 + 编排方案选项');
710
- return { allow: true, systemMessage: card, additionalContext: ForgeFormatter.mergeContexts(ForgeFormatter.wrapInQuote(options), clarify) };
711
- }
712
- logger.info('向用户展示编排方案选项');
713
- return { allow: true, systemMessage: card, additionalContext: ForgeFormatter.wrapInQuote(options) };
714
- }
715
- /**
716
- * 简单任务的轻量注入:只注入规范和画像,不生成执行卡片和阶段计划。
717
- */
718
- buildLightweightResponse(analysis, projectPath) {
719
- const activeConventions = this.ctx.conventionManager.getActiveConventions(projectPath);
720
- const conventionPrompt = this.ctx.conventionManager.getActivePrompt(projectPath) ?? undefined;
721
- if (activeConventions.length > 0) {
722
- logger.info(`[轻量注入] 规范:${ForgeFormatter.formatConventionSummary(activeConventions)}`);
723
- }
724
- const similarSessionCtx = analysis.searchKeywords?.length
725
- ? this.buildSimilarSessionContext(analysis.searchKeywords, projectPath)
726
- : undefined;
727
- const body = ContextInjector.build({ content: this.buildPlanStartContext(projectPath), priority: INJECT_PRIORITY.PLAN, topic: 'plan' }, { content: this.buildProfileContext(projectPath), priority: INJECT_PRIORITY.PROFILE, topic: 'profile' }, { content: similarSessionCtx, priority: INJECT_PRIORITY.HISTORY, minBudget: 500, topic: 'history' }, { content: conventionPrompt, priority: INJECT_PRIORITY.CONVENTION, topic: 'convention' });
728
- const injected = [];
729
- if (this.injectionCache.get(projectPath)?.profileInjected)
730
- injected.push('画像');
731
- if (similarSessionCtx)
732
- injected.push('相似历史场景');
733
- if (conventionPrompt)
734
- injected.push(`规范(${activeConventions.map(c => c.id).join(',')})`);
735
- logger.info(`处理完成:轻量注入 | 复杂度=simple | 注入=[${injected.join(', ') || '无'}]`);
736
- if (!body)
737
- return undefined;
738
- return {
739
- allow: true,
740
- additionalContext: ForgeFormatter.wrapInQuote(body),
741
- };
742
- }
743
- async buildSimpleResponse(analysis, card, projectPath) {
744
- const directive = this.ctx.intentEngine.generateDirective(analysis);
745
- const decisionsContext = await this.readDecisionsContext(analysis.requirement, projectPath);
746
- const similarSessionCtx = analysis.searchKeywords?.length
747
- ? this.buildSimilarSessionContext(analysis.searchKeywords, projectPath)
748
- : undefined;
749
- const graphContext = analysis.searchKeywords?.length
750
- ? await this.buildGraphContext(analysis.searchKeywords, projectPath)
751
- : undefined;
752
- // 加载项目规范并记录
753
- const activeConventions = this.ctx.conventionManager.getActiveConventions(projectPath);
754
- const conventionPrompt = this.ctx.conventionManager.getActivePrompt(projectPath) ?? undefined;
755
- if (activeConventions.length > 0) {
756
- logger.info(`已加载项目规范:${ForgeFormatter.formatConventionSummary(activeConventions)}`);
757
- }
758
- if (analysis.clarifyQuestions?.length) {
759
- const clarify = ForgeFormatter.formatClarifyRequest(analysis.clarifyQuestions);
760
- logger.info(`注入澄清问题 ${analysis.clarifyQuestions.length} 个`);
761
- return {
762
- allow: true,
763
- systemMessage: card,
764
- additionalContext: clarify,
765
- };
766
- }
767
- const body = ContextInjector.build({ content: this.buildPlanStartContext(projectPath), priority: INJECT_PRIORITY.PLAN, topic: 'plan' }, { content: this.buildProfileContext(projectPath), priority: INJECT_PRIORITY.PROFILE, topic: 'profile' }, { content: conventionPrompt, priority: INJECT_PRIORITY.CONVENTION, topic: 'convention' }, { content: directive ?? undefined, priority: INJECT_PRIORITY.DIRECTIVE, topic: 'directive' }, { content: similarSessionCtx, priority: INJECT_PRIORITY.HISTORY, minBudget: 500, topic: 'history' }, { content: graphContext, priority: INJECT_PRIORITY.HISTORY - 1, minBudget: 300, topic: 'graph' }, { content: decisionsContext, priority: INJECT_PRIORITY.DECISIONS, topic: 'decisions' });
768
- // 汇总注入内容
769
- const injected = [];
770
- if (this.injectionCache.get(projectPath)?.profileInjected)
771
- injected.push('画像');
772
- if (conventionPrompt)
773
- injected.push(`规范(${activeConventions.map(c => c.id).join(',')})`);
774
- if (directive)
775
- injected.push('执行指令');
776
- if (decisionsContext)
777
- injected.push('决策历史');
778
- if (similarSessionCtx)
779
- injected.push('相似历史场景');
780
- if (graphContext)
781
- injected.push('图谱成功路径');
782
- const inferredRole = this.inferCurrentRole(analysis.requirement, activeConventions);
783
- const roleStr = inferredRole ? ` | 当前角色=${inferredRole.name}` : '';
784
- logger.info(`处理完成:直接执行 | 复杂度=${analysis.complexity}${roleStr} | 注入=[${injected.join(', ') || '无'}]`);
785
- return {
786
- allow: true,
787
- systemMessage: card,
788
- additionalContext: body ? ForgeFormatter.wrapInQuote(body) : undefined,
789
- };
790
- }
791
- /**
792
- * 知识图谱上下文:查找历史成功路径,注入为参考上下文
793
- */
794
- async buildGraphContext(keywords, projectPath) {
795
- const graph = this.ctx.retrospectiveEngine?.getKnowledgeGraph();
796
- if (!graph || keywords.length === 0)
797
- return undefined;
798
- try {
799
- const paths = await graph.findSimilarSuccessfulPaths(projectPath, keywords, 3);
800
- if (paths.length === 0)
801
- return undefined;
802
- const lines = ['[历史成功路径参考]'];
803
- for (const p of paths) {
804
- lines.push(`- 需求:${p.requirement}`);
805
- if (p.decisions.length > 0)
806
- lines.push(` 决策:${p.decisions.join(' → ')}`);
807
- lines.push(` 结果:${p.outcome}`);
808
- }
809
- return lines.join('\n');
810
- }
811
- catch {
812
- return undefined;
813
- }
814
- }
815
- /**
816
- * FTS5 历史场景检索:按 searchKeywords 查找最相似的历史 session,
817
- * 读取对应 decisions.md 片段注入为"相似历史场景"上下文。
818
- * 每个项目 session 内首次调用时执行;无结果或已注入则返回 undefined。
819
- */
820
- buildSimilarSessionContext(keywords, projectPath) {
821
- if (!keywords || keywords.length === 0)
822
- return undefined;
823
- try {
824
- const { storage } = this.ctx;
825
- const hits = storage.searchByKeywords(keywords, projectPath, 10);
826
- if (hits.length === 0)
827
- return undefined;
828
- // 按 session_id 去重,取最近 3 个不同 session
829
- const sessionIds = [];
830
- for (const ev of hits) {
831
- if (!sessionIds.includes(ev.session_id) && sessionIds.length < 3) {
832
- sessionIds.push(ev.session_id);
833
- }
834
- }
835
- if (sessionIds.length === 0)
836
- return undefined;
837
- // 优先从 injectionCache 取已读取的 rawTail,避免重复 readFileSync
838
- const cache = this.injectionCache.get(projectPath);
839
- let rawTail;
840
- if (cache?.decisionsRawTail) {
841
- rawTail = cache.decisionsRawTail;
842
- }
843
- else {
844
- const fileData = this.readDecisionsFile(projectPath);
845
- if (!fileData)
846
- return undefined;
847
- rawTail = fileData.tail;
848
- }
849
- if (!rawTail.trim())
850
- return undefined;
851
- const snippet = rawTail.length > 600 ? '...\n' + rawTail.slice(-600) : rawTail;
852
- const sessionList = sessionIds.map(id => `- ${id.slice(0, 8)}...`).join('\n');
853
- return `[相似历史场景]\n关键词:${keywords.slice(0, 5).join('、')}\n匹配会话:\n${sessionList}\n\n历史决策摘要:\n\`\`\`\n${snippet}\n\`\`\``;
854
- }
855
- catch {
856
- return undefined;
857
- }
858
- }
859
- /**
860
- * 增量读取 decisions.md 末尾 tailBytes 字节,写入 injectionCache。
861
- * 指纹使用 size:mtimeMs,只需 stat() 即可判断缓存有效性。
862
- * 返回 null 表示文件不存在或读取失败。
863
- */
864
- readDecisionsFile(projectPath, tailBytes = 2048) {
865
- try {
866
- const decisionsPath = path.join(projectPath, '.claude-forge', 'decisions.md');
867
- if (!fs.existsSync(decisionsPath))
868
- return null;
869
- const stat = fs.statSync(decisionsPath);
870
- if (stat.size === 0)
871
- return null;
872
- const readSize = Math.min(tailBytes, stat.size);
873
- const buf = Buffer.alloc(readSize);
874
- const fd = fs.openSync(decisionsPath, 'r');
75
+ const stages = this.createStages();
76
+ const proj = relPath(event.project_path);
77
+ logger.info(`[Forge:编排] 开始 | 项目=${proj} | prompt="${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? '' : ''}"`);
78
+ let stageIdx = 0;
79
+ for (const stage of stages) {
80
+ stageIdx++;
875
81
  try {
876
- fs.readSync(fd, buf, 0, readSize, stat.size - readSize);
877
- }
878
- finally {
879
- fs.closeSync(fd);
880
- }
881
- const fingerprint = `${stat.size}:${stat.mtimeMs}`;
882
- const tail = buf.toString('utf-8');
883
- // 写入缓存(只更新 rawTail 和 fingerprint,不覆盖 decisionsResult)
884
- const cache = this.injectionCache.get(projectPath);
885
- if (!cache || cache.decisionsKey !== fingerprint) {
886
- this.injectionCache.set(projectPath, {
887
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, planContextInjected: false, skillSuggestionsInjected: false }),
888
- decisionsKey: fingerprint,
889
- decisionsRawTail: tail,
890
- });
891
- }
892
- return { tail, fingerprint };
893
- }
894
- catch {
895
- return null;
896
- }
897
- }
898
- /**
899
- * 读取项目决策历史,优先使用树索引推理检索,失败时降级到关键词切片。
900
- * 方案 A:同一 session 内 decisions.md 内容不变则直接复用缓存,不重复注入。
901
- */
902
- async readDecisionsContext(requirement, projectPath) {
903
- // 只在涉及架构/设计/重构时注入,避免噪音
904
- const needsContext = /架构|设计|重构|优化|改造|迁移|升级|architecture|design|refactor/i.test(requirement);
905
- if (!needsContext)
906
- return undefined;
907
- try {
908
- const decisionsPath = path.join(projectPath, '.claude-forge', 'decisions.md');
909
- if (!fs.existsSync(decisionsPath))
910
- return undefined;
911
- // 用 stat 指纹(size:mtimeMs)判断缓存有效性
912
- const stat = fs.statSync(decisionsPath);
913
- const fingerprint = `${stat.size}:${stat.mtimeMs}`;
914
- const cache = this.injectionCache.get(projectPath);
915
- if (cache?.decisionsKey === fingerprint) {
916
- logger.info('[Forge:决策] 内容未变,复用缓存');
917
- return cache.decisionsResult || undefined;
918
- }
919
- // 尝试树索引推理检索
920
- const treeResult = await this.readDecisionsViaTreeIndex(requirement, decisionsPath);
921
- if (treeResult) {
922
- const result = ForgeFormatter.formatModuleNotification('decisions', `项目决策历史(推理检索):\n${treeResult}`);
923
- this.injectionCache.set(projectPath, {
924
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, planContextInjected: false, skillSuggestionsInjected: false }),
925
- decisionsKey: fingerprint,
926
- decisionsResult: result,
927
- decisionsRawTail: '',
928
- });
929
- return result;
930
- }
931
- // 降级:关键词切片
932
- logger.info('[Forge:决策] 树索引不可用,降级到关键词切片');
933
- return this.readDecisionsContextFallback(requirement, projectPath, fingerprint);
934
- }
935
- catch {
936
- return undefined;
937
- }
938
- }
939
- /**
940
- * 树索引推理检索(新方法)
941
- */
942
- async readDecisionsViaTreeIndex(requirement, decisionsPath) {
943
- try {
944
- // 检查树索引是否存在且最新
945
- if (TreeIndexEngine.isStale(decisionsPath)) {
946
- logger.info('[TreeIndex] 索引过期或不存在,跳过推理检索');
947
- return null;
948
- }
949
- const index = TreeIndexEngine.loadFromFile(decisionsPath);
950
- if (!index || index.structure.length === 0)
951
- return null;
952
- const api = this.ctx.intentEngine?.getProvider();
953
- if (!api)
954
- return null;
955
- const engine = new TreeIndexEngine(api);
956
- const result = await engine.search(index, requirement);
957
- if (result && result.length > 50) {
958
- logger.info(`[TreeIndex] 推理检索成功,返回 ${result.length} 字符`);
959
- return result;
960
- }
961
- return null;
962
- }
963
- catch (err) {
964
- logger.warn(`[TreeIndex] 推理检索失败:${err}`);
965
- return null;
966
- }
967
- }
968
- /**
969
- * 降级方案:关键词切片(原有逻辑)
970
- */
971
- readDecisionsContextFallback(requirement, projectPath, fingerprint) {
972
- const cache = this.injectionCache.get(projectPath);
973
- const fileData = this.readDecisionsFile(projectPath);
974
- if (!fileData)
975
- return undefined;
976
- const { tail } = fileData;
977
- if (!tail.trim())
978
- return undefined;
979
- // 按关键词过滤相关决策块
980
- const keywords = requirement
981
- .split(/[\s,,、。.!!??]+/)
982
- .map(w => w.trim())
983
- .filter(w => w.length >= 2);
984
- const blocks = tail.split(/(?=^## )/m).filter(b => b.trim());
985
- let relevant;
986
- if (keywords.length > 0) {
987
- relevant = blocks.filter(block => keywords.some(kw => block.toLowerCase().includes(kw.toLowerCase())));
988
- if (relevant.length === 0)
989
- relevant = blocks.slice(-2);
990
- }
991
- else {
992
- relevant = blocks.slice(-2);
993
- }
994
- let snippet = relevant.join('\n').trim();
995
- if (snippet.length > 600)
996
- snippet = '...\n' + snippet.slice(-600);
997
- const result = ForgeFormatter.formatModuleNotification('decisions', `项目决策历史(关键词匹配):\n\`\`\`\n${snippet}\n\`\`\``);
998
- this.injectionCache.set(projectPath, {
999
- ...(cache ?? { decisionsKey: '', decisionsResult: '', decisionsRawTail: '', profileInjected: false, planContextInjected: false, skillSuggestionsInjected: false }),
1000
- decisionsKey: fingerprint,
1001
- decisionsResult: result,
1002
- decisionsRawTail: tail,
1003
- });
1004
- logger.info(`[Forge:决策] 关键词匹配注入 ${relevant.length} 个块(${snippet.length} 字符)`);
1005
- return result;
1006
- }
1007
- /**
1008
- * 处理用户对 Pipeline 选择的回复(A/B/C)
1009
- */
1010
- async startPipelineFromChoice(choice, event) {
1011
- const option = PIPELINE_OPTIONS[choice];
1012
- if (choice === 'manual') {
1013
- const msg = ForgeFormatter.formatModuleNotification('pipeline', '已切换到手动控制模式,请逐步告诉我你的需求');
1014
- logger.info('用户选择了手动模式');
1015
- return { allow: true, additionalContext: ForgeFormatter.wrapInQuote(msg) };
1016
- }
1017
- const requirement = event.tool_input?.user_prompt || '用户需求';
1018
- const pipeline = await this.ctx.pipelineEngine.startPipeline(requirement, event.project_path, event.session_id);
1019
- if (this.ctx.qualityGate)
1020
- this.ctx.qualityGate.setRequirement(requirement);
1021
- logger.info(`已启动编排(${option.description}):id=${pipeline.id} | 任务数=${pipeline.tasks.length}`);
1022
- const msg = ForgeFormatter.formatModuleNotification('pipeline', `已启动${option.description},共 ${pipeline.tasks.length} 个任务\n阶段:${option.phases.join(' → ')}`);
1023
- return { allow: true, additionalContext: ForgeFormatter.wrapInQuote(msg) };
1024
- }
1025
- /**
1026
- * 根据用户需求关键词推断当前最匹配的 Convention 角色。
1027
- * 纯关键词匹配,不调用 AI,用于日志展示。
1028
- */
1029
- inferCurrentRole(requirement, conventions) {
1030
- const input = requirement.toLowerCase();
1031
- // 角色关键词映射(按优先级排序,先匹配先返回)
1032
- const roleKeywords = [
1033
- // 需求/产品阶段
1034
- { keywords: ['需求', '用户故事', '产品', '规格', '验收', 'spec', 'requirement'], roleKeys: ['product_manager'] },
1035
- // 架构/设计阶段
1036
- { keywords: ['架构', '设计', '方案', '技术选型', 'api', '数据模型', 'schema'], roleKeys: ['architect'] },
1037
- // 测试/安全网阶段
1038
- { keywords: ['测试', '覆盖', '安全网', 'test', '特征测试', '边界'], roleKeys: ['safety_net_engineer', 'verification_engineer'] },
1039
- // 审查阶段
1040
- { keywords: ['审查', 'review', '代码审查', '检查', '质量'], roleKeys: ['tech_lead', 'verification_engineer'] },
1041
- // 理解/分析阶段
1042
- { keywords: ['理解', '分析', '阅读', '调查', '排查', '诊断', 'debug'], roleKeys: ['code_archaeologist'] },
1043
- // 编码/实现阶段(最宽泛,放最后)
1044
- { keywords: ['实现', '编码', '开发', '修复', '重构', '优化', '改', '加', '写', 'fix', 'implement', 'refactor'], roleKeys: ['developer', 'surgeon'] },
1045
- ];
1046
- // 收集所有 Convention 的角色
1047
- const allRoles = new Map();
1048
- for (const conv of conventions) {
1049
- for (const [key, role] of Object.entries(conv.roles ?? {})) {
1050
- allRoles.set(key, role);
1051
- }
1052
- }
1053
- if (allRoles.size === 0)
1054
- return null;
1055
- // 按关键词匹配
1056
- for (const { keywords, roleKeys } of roleKeywords) {
1057
- if (keywords.some(kw => input.includes(kw))) {
1058
- for (const rk of roleKeys) {
1059
- const role = allRoles.get(rk);
1060
- if (role)
1061
- return role;
82
+ const result = await stage.execute(ctx);
83
+ if (result.shouldTerminate) {
84
+ logger.info(`[Forge:编排] ✓ [${stageIdx}/${stages.length}] ${stage.name} → 终止${result.response ? '(有响应)' : '(无响应)'}`);
85
+ return result.response ?? undefined;
1062
86
  }
1063
- }
1064
- }
1065
- // 默认返回开发工程师(最常见的角色)
1066
- return allRoles.get('developer') ?? allRoles.get('surgeon') ?? null;
1067
- }
1068
- /**
1069
- * P4:负向重启回溯修正
1070
- * 检测到 dissatisfied 信号时,将上一个 session(24h 内)的满意度评分下调(×0.5,最低 0.1)
1071
- */
1072
- async applyNegativeRestartPenalty(projectPath, currentSessionId) {
1073
- try {
1074
- const last = this.ctx.storage.getLastCompletedTaskSession(projectPath);
1075
- if (!last)
1076
- return;
1077
- if (last.session_id === currentSessionId)
1078
- return;
1079
- // 只回溯 24h 内的上一 session
1080
- if (last.ended_at) {
1081
- const age = Date.now() - new Date(last.ended_at).getTime();
1082
- if (age > 24 * 60 * 60 * 1000)
1083
- return;
1084
- }
1085
- const prevScore = last.satisfaction_score ?? 0.5;
1086
- const newScore = Math.max(0.1, prevScore * 0.5);
1087
- // 更新 task_sessions.satisfaction_score
1088
- this.ctx.storage.updateTaskSessionSatisfaction(last.id, newScore);
1089
- // 写入 satisfaction_signals 记录此次回溯修正
1090
- this.ctx.storage.insertSatisfactionSignal({
1091
- id: `ss_restart_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1092
- session_id: last.session_id,
1093
- project_path: projectPath,
1094
- task_session_id: last.id,
1095
- signal_type: 'negative_restart_penalty',
1096
- signal_value: newScore - prevScore, // 负数,表示下调幅度
1097
- weight: 1.0,
1098
- raw_data: { prev_score: prevScore, new_score: newScore, triggered_by_session: currentSessionId },
1099
- });
1100
- logger.info(`[Forge:满意度] 检测到负向重启,回溯修正上一 session 评分:${prevScore.toFixed(3)} → ${newScore.toFixed(3)}`);
1101
- }
1102
- catch (err) {
1103
- logger.warn(`[Forge:满意度] 负向重启回溯修正失败:${err}`);
1104
- }
1105
- }
1106
- /** 构建用户上下文(供 IntentEngine 有状态分析) */
1107
- buildUserContext(projectPath) {
1108
- const profile = this.ctx.profileManager.get();
1109
- const recentSatisfaction = this.ctx.storage.getSatisfactionHistory(projectPath, 10);
1110
- const avgSatisfaction = recentSatisfaction.length > 0
1111
- ? recentSatisfaction.reduce((sum, row) => sum + row.score, 0) / recentSatisfaction.length
1112
- : 0.5;
1113
- const complexityCount = { simple: 0, moderate: 0, complex: 0 };
1114
- for (const row of recentSatisfaction) {
1115
- if (row.complexity === 'simple' || row.complexity === 'moderate' || row.complexity === 'complex') {
1116
- complexityCount[row.complexity]++;
1117
- }
1118
- }
1119
- const preferredComplexity = (Object.entries(complexityCount).sort((a, b) => b[1] - a[1])[0]?.[0] ?? 'moderate');
1120
- const experienceLevelMap = {
1121
- junior: 'beginner',
1122
- mid: 'intermediate',
1123
- senior: 'intermediate',
1124
- expert: 'expert',
1125
- };
1126
- return {
1127
- work_style: profile.work_style ?? 'balanced',
1128
- experience_level: experienceLevelMap[profile.experience_level ?? 'mid'],
1129
- skip_rate: profile.skip_rate ?? 0,
1130
- override_rate: profile.override_rate ?? 0,
1131
- avg_satisfaction: avgSatisfaction,
1132
- preferred_complexity: preferredComplexity,
1133
- };
87
+ if (result.skipRemaining) {
88
+ logger.info(`[Forge:编排] ✓ [${stageIdx}/${stages.length}] ${stage.name} → 跳过剩余`);
89
+ break;
90
+ }
91
+ logger.debug(`[Forge:编排] · [${stageIdx}/${stages.length}] ${stage.name} → 继续`);
92
+ }
93
+ catch (err) {
94
+ logger.error(`[Forge:编排] [${stageIdx}/${stages.length}] ${stage.name} 执行失败:${err}`);
95
+ }
96
+ }
97
+ logger.info(`[Forge:编排] 完成 | 项目=${proj} | 所有 Stage 通过,无响应`);
98
+ }
99
+ createStages() {
100
+ return [
101
+ new FailureSignalStage(),
102
+ new ActiveInterventionStage(),
103
+ new InitPromptStage(),
104
+ new SkillSuggestionsStage(),
105
+ new ConversationalConfigStage(this.ctx),
106
+ new EngineCheckStage(),
107
+ new PipelineReplyStage(),
108
+ new EscInterruptStage(),
109
+ new PipelineActiveStage(),
110
+ new CooldownStage(),
111
+ new IntentAnalysisStage(),
112
+ new StrategyAdviceStage(),
113
+ new TemplateRouteStage(),
114
+ new PlanResumeStage(),
115
+ new PlanEnforcementStage(),
116
+ new InterventionLevelStage(),
117
+ new SimpleTaskStage(),
118
+ new ComplexTaskStage(),
119
+ new ModerateTaskStage(),
120
+ ];
1134
121
  }
1135
122
  }
1136
123
  //# sourceMappingURL=user-prompt-handler.js.map