@kkelly-offical/kkcode 0.1.3 → 0.1.7

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 (66) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +220 -170
  4. package/src/agent/prompt/bug-hunter.txt +90 -0
  5. package/src/agent/prompt/frontend-designer.txt +58 -0
  6. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  7. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  8. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  9. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  10. package/src/config/defaults.mjs +260 -195
  11. package/src/config/schema.mjs +71 -6
  12. package/src/core/constants.mjs +91 -46
  13. package/src/index.mjs +1 -1
  14. package/src/knowledge/frontend-aesthetics.txt +39 -0
  15. package/src/knowledge/loader.mjs +2 -1
  16. package/src/knowledge/tailwind.txt +12 -3
  17. package/src/mcp/client-http.mjs +141 -157
  18. package/src/mcp/client-sse.mjs +288 -286
  19. package/src/mcp/client-stdio.mjs +533 -451
  20. package/src/mcp/constants.mjs +2 -0
  21. package/src/mcp/registry.mjs +479 -394
  22. package/src/mcp/stdio-framing.mjs +133 -127
  23. package/src/mcp/tool-result.mjs +24 -0
  24. package/src/observability/index.mjs +42 -0
  25. package/src/observability/metrics.mjs +137 -0
  26. package/src/observability/tracer.mjs +137 -0
  27. package/src/orchestration/background-manager.mjs +372 -358
  28. package/src/orchestration/background-worker.mjs +305 -245
  29. package/src/orchestration/longagent-manager.mjs +171 -116
  30. package/src/orchestration/stage-scheduler.mjs +728 -489
  31. package/src/permission/exec-policy.mjs +9 -11
  32. package/src/provider/anthropic.mjs +1 -0
  33. package/src/provider/openai.mjs +340 -339
  34. package/src/provider/retry-policy.mjs +68 -68
  35. package/src/provider/router.mjs +241 -228
  36. package/src/provider/sse.mjs +104 -91
  37. package/src/repl.mjs +59 -7
  38. package/src/session/checkpoint.mjs +66 -3
  39. package/src/session/compaction.mjs +298 -276
  40. package/src/session/engine.mjs +232 -225
  41. package/src/session/longagent-4stage.mjs +460 -0
  42. package/src/session/longagent-hybrid.mjs +1097 -0
  43. package/src/session/longagent-plan.mjs +365 -329
  44. package/src/session/longagent-project-memory.mjs +53 -0
  45. package/src/session/longagent-scaffold.mjs +291 -100
  46. package/src/session/longagent-task-bus.mjs +54 -0
  47. package/src/session/longagent-utils.mjs +472 -0
  48. package/src/session/longagent.mjs +900 -1462
  49. package/src/session/loop.mjs +65 -40
  50. package/src/session/project-context.mjs +30 -0
  51. package/src/session/prompt/agent.txt +25 -0
  52. package/src/session/prompt/plan.txt +31 -9
  53. package/src/session/rollback.mjs +196 -0
  54. package/src/session/store.mjs +519 -503
  55. package/src/session/system-prompt.mjs +273 -260
  56. package/src/session/task-validator.mjs +4 -3
  57. package/src/skill/builtin/design.mjs +76 -0
  58. package/src/skill/builtin/frontend.mjs +8 -0
  59. package/src/skill/registry.mjs +390 -336
  60. package/src/storage/ghost-commit-store.mjs +18 -8
  61. package/src/tool/executor.mjs +11 -0
  62. package/src/tool/git-auto.mjs +0 -19
  63. package/src/tool/question-prompt.mjs +93 -86
  64. package/src/tool/registry.mjs +71 -37
  65. package/src/ui/activity-renderer.mjs +664 -410
  66. package/src/util/git.mjs +23 -0
@@ -0,0 +1,1097 @@
1
+ /**
2
+ * LongAgent Hybrid 模式
3
+ * 融合 4-Stage 的只读探索/规划/调试回滚 + Parallel 的脚手架/并行执行/门控
4
+ *
5
+ * 流程: H0:Intake → H1:Preview → H2:Blueprint → H2.5:Git → H3:Scaffold → H4:Coding(并行) → H5:Debugging(回滚) → H5.5:Validation → H6:Gates → H7:GitMerge
6
+ */
7
+ import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
8
+ import { processTurnLoop } from "./loop.mjs"
9
+ import { markSessionStatus } from "./store.mjs"
10
+ import { EventBus } from "../core/events.mjs"
11
+ import { EVENT_TYPES, LONGAGENT_4STAGE_STAGES } from "../core/constants.mjs"
12
+ import { saveCheckpoint, loadCheckpoint, saveTaskCheckpoint, loadTaskCheckpoints, cleanupCheckpoints } from "./checkpoint.mjs"
13
+ import { getAgent } from "../agent/agent.mjs"
14
+ import { runStageBarrier } from "../orchestration/stage-scheduler.mjs"
15
+ import { runScaffoldPhase } from "./longagent-scaffold.mjs"
16
+ import {
17
+ runUsabilityGates,
18
+ hasGatePreferences,
19
+ getGatePreferences,
20
+ saveGatePreferences,
21
+ buildGatePromptText,
22
+ parseGateSelection
23
+ } from "./usability-gates.mjs"
24
+ import { runIntakeDialogue, validateAndNormalizeStagePlan, defaultStagePlan } from "./longagent-plan.mjs"
25
+ import { createValidator } from "./task-validator.mjs"
26
+ import { detectStageComplete, detectReturnToCoding, buildStageWrapper } from "./longagent-4stage.mjs"
27
+ import {
28
+ isComplete,
29
+ isLikelyActionableObjective,
30
+ mergeCappedFileChanges,
31
+ stageProgressStats,
32
+ summarizeGateFailures,
33
+ LONGAGENT_FILE_CHANGES_LIMIT,
34
+ createStuckTracker,
35
+ classifyError,
36
+ ERROR_CATEGORIES,
37
+ createSemanticErrorTracker,
38
+ createDegradationChain,
39
+ generateRecoverySuggestions,
40
+ stripFence,
41
+ parseJsonLoose
42
+ } from "./longagent-utils.mjs"
43
+ import { TaskBus } from "./longagent-task-bus.mjs"
44
+ import { loadProjectMemory, saveProjectMemory, memoryToContext, parseMemoryFromPreview } from "./longagent-project-memory.mjs"
45
+ import * as git from "../util/git.mjs"
46
+
47
+ // Checkpoint 结构校验
48
+ function validateCheckpoint(cp) {
49
+ if (!cp || !cp.stagePlan || !Array.isArray(cp.stagePlan.stages)) return false
50
+ if (typeof cp.stageIndex !== "number" || cp.stageIndex < 0) return false
51
+ if (cp.stageIndex > cp.stagePlan.stages.length) return false
52
+ // Verify the previous stage exists for task checkpoint loading
53
+ if (cp.stageIndex > 0 && !cp.stagePlan.stages[cp.stageIndex - 1]) return false
54
+ return true
55
+ }
56
+
57
+ // Gate 修复策略路由 (Phase 8)
58
+ function getGateFixStrategy(failures) {
59
+ const gateTypes = (failures || []).map(f => f.gate).filter(Boolean)
60
+ if (gateTypes.includes("test")) return { agent: "debugging-agent", prefix: "Analyze test failures and fix:" }
61
+ if (gateTypes.every(g => g === "build")) return { agent: "coding-agent", prefix: "Fix build errors:" }
62
+ if (gateTypes.every(g => g === "lint")) return { autoFix: "npx eslint --fix .", agent: "coding-agent", prefix: "Fix lint errors:" }
63
+ return { agent: "coding-agent", prefix: "Fix gate failures:" }
64
+ }
65
+
66
+ // #13 上下文压缩
67
+ async function compressContext(text, limit, { model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, toolContext }) {
68
+ if (text.length <= limit) return text
69
+ const out = await processTurnLoop({
70
+ prompt: [
71
+ `Compress the following engineering context to max ${Math.round(limit * 0.6)} characters.`,
72
+ "Preserve ONLY:",
73
+ "- Concrete decisions made (technology choices, architecture patterns, API contracts)",
74
+ "- File paths and function signatures that were created or modified",
75
+ "- Error messages and their resolutions",
76
+ "- Cross-task dependencies and integration points",
77
+ "- Test results (pass/fail with specific failure reasons)",
78
+ "Discard: exploration logs, verbose tool output, repeated information, reasoning chains.",
79
+ "Output the compressed context directly — no preamble or explanation.",
80
+ "",
81
+ text.slice(0, limit * 2)
82
+ ].join("\n"),
83
+ mode: "ask", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, allowQuestion: false, toolContext
84
+ })
85
+ return (out.reply || text.slice(0, limit)).slice(0, limit)
86
+ }
87
+
88
+ // #3 动态计划修订解析
89
+ function parseReplanMarker(text) {
90
+ const match = String(text || "").match(/\[REPLAN:\s*([\s\S]*?)\]/i)
91
+ if (!match) return null
92
+ try { return JSON.parse(match[1]) } catch { return null }
93
+ }
94
+
95
+ // #1 细粒度回滚:从 debugging 输出中提取失败的 taskId
96
+ function extractFailedTaskIds(text) {
97
+ const ids = []
98
+ const pattern = /\[FAILED_TASK:\s*(\S+)\]/gi
99
+ let m
100
+ while ((m = pattern.exec(text)) !== null) ids.push(m[1])
101
+ return ids
102
+ }
103
+
104
+
105
+ function parseBlueprintOutput(reply, objective, defaults) {
106
+ // 1. 尝试提取 ```stage_plan_json ... ``` 块
107
+ const jsonMatch = reply.match(/```stage_plan_json\s*([\s\S]*?)```/)
108
+ if (jsonMatch) {
109
+ const parsed = parseJsonLoose(jsonMatch[1])
110
+ if (parsed?.stages) {
111
+ const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
112
+ if (!errors.length) {
113
+ return { architectureText: reply.replace(/```stage_plan_json[\s\S]*?```/g, "").trim(), stagePlan: plan }
114
+ }
115
+ }
116
+ }
117
+ // 2. 回退:尝试任意 JSON 块
118
+ const anyJson = reply.match(/```(?:json)?\s*([\s\S]*?)```/g)
119
+ if (anyJson) {
120
+ for (const block of anyJson) {
121
+ const inner = block.replace(/```(?:json|stage_plan_json)?\s*/g, "").replace(/```/g, "").trim()
122
+ const parsed = parseJsonLoose(inner)
123
+ if (parsed?.stages) {
124
+ const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
125
+ if (!errors.length) return { architectureText: reply, stagePlan: plan }
126
+ }
127
+ }
128
+ }
129
+ // 3. 最终回退:单任务默认计划
130
+ return { architectureText: reply, stagePlan: defaultStagePlan(objective, defaults) }
131
+ }
132
+
133
+ export async function runHybridLongAgent({
134
+ prompt, model, providerType, sessionId, configState,
135
+ baseUrl = null, apiKeyEnv = null, agent = null,
136
+ maxIterations = 0, signal = null, output = null,
137
+ allowQuestion = true, toolContext = {}
138
+ }) {
139
+ const longagentConfig = configState.config.agent.longagent || {}
140
+ const hybridConfig = longagentConfig.hybrid || {}
141
+ const parallelConfig = longagentConfig.parallel || {}
142
+ const gitConfig = longagentConfig.git || {}
143
+ const noProgressLimit = Number(longagentConfig.no_progress_limit || 5)
144
+ const maxGateAttempts = Number(longagentConfig.max_gate_attempts || 5)
145
+ const fileChangesLimit = Math.max(20, Number(longagentConfig.file_changes_limit || LONGAGENT_FILE_CHANGES_LIMIT))
146
+
147
+ // 每阶段模型选择
148
+ const separateModels = hybridConfig.separate_models || {}
149
+ const useSeparateModels = separateModels.enabled === true
150
+ const adaptiveModels = hybridConfig.adaptive_models || {}
151
+ const useAdaptiveModels = adaptiveModels.enabled === true
152
+ function getModelForStage(stage) {
153
+ if (!useSeparateModels) return { model, providerType }
154
+ const m = { preview: separateModels.preview_model, blueprint: separateModels.blueprint_model, debugging: separateModels.debugging_model }
155
+ return m[stage] ? { model: m[stage], providerType } : { model, providerType }
156
+ }
157
+ // #8 自适应模型路由:根据 task complexity 选择模型
158
+ function getModelForTask(task) {
159
+ if (!useAdaptiveModels) return model
160
+ const tier = task?.complexity || "medium"
161
+ return adaptiveModels[tier] || model
162
+ }
163
+
164
+ let iteration = 0, recoveryCount = 0, stageIndex = 0
165
+ let currentPhase = "H0", currentGate = "init"
166
+ let gateStatus = {}, lastGateFailures = []
167
+ let lastProgress = { percentage: 0, currentStep: 0, totalSteps: 0 }
168
+ let finalReply = "", planFrozen = false, stagePlan = null
169
+ let taskProgress = {}, fileChanges = []
170
+ let completionMarkerSeen = false
171
+ let gitBranch = null, gitBaseBranch = null, gitActive = false
172
+ const aggregateUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
173
+ const toolEvents = []
174
+ const startTime = Date.now()
175
+ const stuckTracker = createStuckTracker()
176
+ // Phase 6: 降级链
177
+ const degradationChain = createDegradationChain(hybridConfig.degradation || {})
178
+ // Phase 2: 阶段超时配置
179
+ const codingPhaseTimeoutMs = Number(hybridConfig.coding_phase_timeout_ms || 1800000)
180
+ const debuggingPhaseTimeoutMs = Number(hybridConfig.debugging_phase_timeout_ms || 600000)
181
+ // #4 TaskBus
182
+ const taskBus = hybridConfig.task_bus !== false ? new TaskBus() : null
183
+ // #5 Project Memory
184
+ const cwd = process.cwd()
185
+ let projectMemory = null
186
+ if (hybridConfig.project_memory !== false) {
187
+ try { projectMemory = await loadProjectMemory(cwd) } catch { projectMemory = null }
188
+ }
189
+
190
+ function accumulateUsage(turn) {
191
+ aggregateUsage.input += turn.usage?.input || 0
192
+ aggregateUsage.output += turn.usage?.output || 0
193
+ aggregateUsage.cacheRead += turn.usage?.cacheRead || 0
194
+ aggregateUsage.cacheWrite += turn.usage?.cacheWrite || 0
195
+ if (turn.toolEvents?.length) toolEvents.push(...turn.toolEvents)
196
+ }
197
+
198
+ async function setPhase(next, reason = "") {
199
+ if (currentPhase === next) return
200
+ const prev = currentPhase
201
+ currentPhase = next
202
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_CHANGED, sessionId, payload: { prevPhase: prev, nextPhase: next, reason, iteration } })
203
+ }
204
+
205
+ async function syncState(patch = {}) {
206
+ const stats = stageProgressStats(taskProgress)
207
+ await LongAgentManager.update(sessionId, {
208
+ status: patch.status || "running", phase: currentPhase, gateStatus, currentGate,
209
+ recoveryCount, lastGateFailures, iterations: iteration, heartbeatAt: Date.now(),
210
+ progress: lastProgress, planFrozen, stageIndex,
211
+ stageCount: stagePlan?.stages?.length || 0,
212
+ taskProgress, stageProgress: { done: stats.done, total: stats.total },
213
+ remainingFilesCount: stats.remainingFilesCount,
214
+ ...patch
215
+ })
216
+ }
217
+
218
+ await markSessionStatus(sessionId, "running-longagent")
219
+ await syncState({ status: "running", lastMessage: "hybrid mode started" })
220
+
221
+ // 前置检查
222
+ if (!isLikelyActionableObjective(prompt)) {
223
+ const blocked = "LongAgent 需要明确的编码目标。请直接描述要实现/修复的内容。"
224
+ await LongAgentManager.update(sessionId, { status: "blocked", phase: "H0", lastMessage: blocked })
225
+ await markSessionStatus(sessionId, "active")
226
+ return { sessionId, turnId: `turn_long_${Date.now()}`, reply: blocked, usage: aggregateUsage, toolEvents, iterations: 0, status: "blocked", phase: "H0", gateStatus: {}, currentGate: "init", lastGateFailures: [], recoveryCount: 0, progress: lastProgress, elapsed: 0, stageIndex: 0, stageCount: 0, planFrozen: false, taskProgress: {}, fileChanges: [], stageProgress: { done: 0, total: 0 }, remainingFilesCount: 0 }
227
+ }
228
+
229
+ // #15 Checkpoint 恢复:如果有之前的检查点,跳过已完成阶段
230
+ // #22: 增强为 task 级粒度恢复
231
+ if (hybridConfig.checkpoint_resume !== false) {
232
+ try {
233
+ const cp = await loadCheckpoint(sessionId)
234
+ if (cp?.stageIndex > 0 && cp?.stagePlan) {
235
+ if (!validateCheckpoint(cp)) {
236
+ // Invalid checkpoint structure — discard and start fresh
237
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_INVALID, sessionId, payload: { reason: "structure_validation_failed" } })
238
+ } else {
239
+ stagePlan = cp.stagePlan; stageIndex = cp.stageIndex; planFrozen = true
240
+ taskProgress = cp.taskProgress || {}; lastProgress = cp.lastProgress || lastProgress
241
+ iteration = cp.iteration || 0
242
+ // #22: Load task-level checkpoints to recover intra-stage progress
243
+ if (stageIndex > 0) {
244
+ const prevStage = cp.stagePlan.stages[stageIndex - 1]
245
+ if (prevStage) {
246
+ const taskCps = await loadTaskCheckpoints(sessionId, prevStage.stageId)
247
+ for (const [tid, tData] of Object.entries(taskCps)) {
248
+ if (!taskProgress[tid] || taskProgress[tid].status !== "completed") {
249
+ taskProgress[tid] = { ...taskProgress[tid], ...tData }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_RESUMED, sessionId, payload: { stageIndex, iteration } })
255
+ await syncState({ lastMessage: `resumed from checkpoint at stage ${stageIndex}` })
256
+ }
257
+ }
258
+ } catch { /* no checkpoint, start fresh */ }
259
+ }
260
+
261
+ // #5 Memory 事件
262
+ if (projectMemory?.techStack?.length) {
263
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_LOADED, sessionId, payload: { techStack: projectMemory.techStack } })
264
+ }
265
+
266
+ // ========== H0: INTAKE (需求澄清) ==========
267
+ let intakeSummary = prompt
268
+ if (hybridConfig.intake !== false && !planFrozen) {
269
+ await setPhase("H0", "intake")
270
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_INTAKE_STARTED, sessionId, payload: { objective: prompt } })
271
+ await syncState({ lastMessage: "H0: intake dialogue — clarifying requirements" })
272
+
273
+ const plannerConfig = longagentConfig.planner || {}
274
+ const intakeConfig = plannerConfig.intake_questions || {}
275
+ const intake = await runIntakeDialogue({
276
+ objective: prompt,
277
+ model, providerType, sessionId, configState,
278
+ baseUrl, apiKeyEnv, agent, signal,
279
+ maxRounds: Number(intakeConfig.max_rounds || 6)
280
+ })
281
+ intakeSummary = intake.summary || prompt
282
+ accumulateUsage(intake)
283
+ gateStatus.intake = { status: "pass", rounds: intake.transcript.length, summary: intakeSummary.slice(0, 500) }
284
+ await syncState({ lastMessage: `H0: intake complete (${intake.transcript.length} qa pairs)` })
285
+ }
286
+
287
+ // ========== H1: PREVIEW (只读探索) ==========
288
+ await setPhase("H1", "preview")
289
+ currentGate = "preview"
290
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_START, sessionId, payload: { objective: prompt } })
291
+ await syncState({ lastMessage: "H1: preview agent exploring codebase" })
292
+
293
+ const previewModel = getModelForStage("preview")
294
+ // #5 注入 project memory 到 preview prompt
295
+ const memCtx = projectMemory ? memoryToContext(projectMemory) : ""
296
+ const previewPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.PREVIEW, { preview: null, blueprint: null, coding: null }, memCtx ? `${memCtx}\n\n${intakeSummary}` : intakeSummary)
297
+ const previewOut = await processTurnLoop({
298
+ prompt: previewPrompt, mode: "agent", agent: getAgent("preview-agent"),
299
+ model: previewModel.model, providerType: previewModel.providerType,
300
+ sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
301
+ })
302
+ accumulateUsage(previewOut)
303
+ const previewFindings = previewOut.reply || ""
304
+
305
+ gateStatus.preview = { status: "pass", findingsLength: previewFindings.length }
306
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_COMPLETE, sessionId, payload: { findingsLength: previewFindings.length } })
307
+ await syncState({ lastMessage: `H1: preview complete (${previewFindings.length} chars)` })
308
+
309
+ // ========== H2: BLUEPRINT (只读规划 + 结构化 stagePlan) ==========
310
+ await setPhase("H2", "blueprint")
311
+ currentGate = "blueprint"
312
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_START, sessionId, payload: {} })
313
+ await syncState({ lastMessage: "H2: blueprint agent designing architecture" })
314
+
315
+ const blueprintModel = getModelForStage("blueprint")
316
+ const blueprintPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.BLUEPRINT, { preview: previewFindings, blueprint: null, coding: null }, prompt)
317
+ + [
318
+ "\n\n## HYBRID MODE: STRUCTURED EXECUTION PLAN (REQUIRED)",
319
+ "In addition to your architecture design, you MUST output a machine-parseable stage plan.",
320
+ "",
321
+ "Wrap it in a ```stage_plan_json ... ``` fenced block. Schema:",
322
+ '{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","passRule":"all_success","tasks":[{"taskId":"...","prompt":"detailed task prompt for sub-agent","plannedFiles":["file1.mjs","file2.mjs"],"acceptance":["node --check file1.mjs","node --test test/file1.test.mjs"],"timeoutMs":600000,"maxRetries":2,"complexity":"low|medium|high"}]}]}',
323
+ "",
324
+ "Rules for the stage plan:",
325
+ "- Each task prompt must be SELF-CONTAINED: the sub-agent has NO access to your blueprint text",
326
+ "- plannedFiles must list EVERY file the task will create or modify (no file in multiple tasks)",
327
+ "- acceptance must be machine-verifiable commands (not subjective criteria)",
328
+ "- Files that import each other MUST be in the same task",
329
+ "- A module and its test file MUST be in the same task",
330
+ "- Order stages by dependency: shared types → core logic → integration → validation"
331
+ ].join("\n")
332
+ const blueprintOut = await processTurnLoop({
333
+ prompt: blueprintPrompt, mode: "agent", agent: getAgent("blueprint-agent"),
334
+ model: blueprintModel.model, providerType: blueprintModel.providerType,
335
+ sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
336
+ })
337
+ accumulateUsage(blueprintOut)
338
+
339
+ const planDefaults = { timeoutMs: Number(parallelConfig.task_timeout_ms || 600000), maxRetries: Number(parallelConfig.task_max_retries ?? 2) }
340
+ const { architectureText, stagePlan: parsedPlan } = parseBlueprintOutput(blueprintOut.reply || "", prompt, planDefaults)
341
+ stagePlan = parsedPlan
342
+ planFrozen = true
343
+
344
+ gateStatus.blueprint = { status: "pass", hasArchitecture: architectureText.length > 100, stageCount: stagePlan.stages.length }
345
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_COMPLETE, sessionId, payload: { planId: stagePlan.planId, stageCount: stagePlan.stages.length } })
346
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PLAN_FROZEN, sessionId, payload: { planId: stagePlan.planId, stageCount: stagePlan.stages.length, errors: [] } })
347
+ await syncState({ planFrozen: true, lastMessage: `H2: blueprint complete, ${stagePlan.stages.length} stage(s)` })
348
+
349
+ // #9 Blueprint 语义验证
350
+ if (hybridConfig.blueprint_validation !== false && stagePlan.stages.length > 0) {
351
+ const totalTasks = stagePlan.stages.reduce((s, st) => s + (st.tasks?.length || 0), 0)
352
+ const totalFiles = new Set(stagePlan.stages.flatMap(st => (st.tasks || []).flatMap(t => t.plannedFiles || []))).size
353
+ const valid = totalTasks > 0 && totalFiles > 0
354
+ gateStatus.blueprintValidation = { status: valid ? "pass" : "warn", totalTasks, totalFiles }
355
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_VALIDATED, sessionId, payload: { totalTasks, totalFiles, valid } })
356
+ }
357
+
358
+ // #2 人工审查检查点
359
+ if (hybridConfig.blueprint_review === true && allowQuestion) {
360
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_REVIEW, sessionId, payload: { planId: stagePlan.planId } })
361
+ const reviewOut = await processTurnLoop({
362
+ prompt: `[SYSTEM] Blueprint 已生成,包含 ${stagePlan.stages.length} 个阶段。架构摘要:\n${architectureText.slice(0, 1500)}\n\n请确认是否继续执行?回复 yes/是 继续,no/否 中止。`,
363
+ mode: "ask", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
364
+ })
365
+ accumulateUsage(reviewOut)
366
+ const answer = String(reviewOut.reply || "").toLowerCase().trim()
367
+ if (["no", "否", "n", "取消", "abort"].some(k => answer.includes(k))) {
368
+ await LongAgentManager.update(sessionId, { status: "aborted", lastMessage: "user rejected blueprint" })
369
+ await markSessionStatus(sessionId, "active")
370
+ return { sessionId, turnId: `turn_long_${Date.now()}`, reply: "用户中止了 Blueprint 审查。", usage: aggregateUsage, toolEvents, iterations: iteration, status: "aborted", phase: "H2", gateStatus, currentGate, lastGateFailures: [], recoveryCount: 0, progress: lastProgress, elapsed: Math.round((Date.now() - startTime) / 1000), stageIndex: 0, stageCount: stagePlan.stages.length, planFrozen, taskProgress: {}, fileChanges: [], stageProgress: { done: 0, total: 0 }, remainingFilesCount: 0 }
371
+ }
372
+ }
373
+
374
+ // ========== H2.5: GIT BRANCH (可选) ==========
375
+ const gitEnabled = gitConfig.enabled === true || gitConfig.enabled === "ask"
376
+ const gitAsk = gitConfig.enabled === "ask"
377
+ const inGitRepo = gitEnabled && await git.isGitRepo(cwd)
378
+
379
+ if (inGitRepo) {
380
+ await setPhase("H2.5", "git_branch")
381
+ let userWantsGit = !gitAsk
382
+ if (gitAsk && allowQuestion) {
383
+ const askResult = await processTurnLoop({
384
+ prompt: "[SYSTEM] 是否为本次 Hybrid LongAgent 创建独立 Git 分支?回复 yes/是 启用,no/否 跳过。",
385
+ mode: "ask", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
386
+ })
387
+ const answer = String(askResult.reply || "").toLowerCase().trim()
388
+ userWantsGit = ["yes", "是", "y", "ok", "好", "确认"].some(k => answer.includes(k))
389
+ accumulateUsage(askResult)
390
+ }
391
+ if (userWantsGit) {
392
+ gitBaseBranch = await git.currentBranch(cwd)
393
+ // Guard: skip git flow if branch is empty or HEAD detached
394
+ if (!gitBaseBranch || gitBaseBranch === "HEAD") {
395
+ gateStatus.git = { status: "warn", reason: "detached HEAD or no branch" }
396
+ } else {
397
+ const branchName = git.generateBranchName(sessionId, prompt)
398
+ const clean = await git.isClean(cwd)
399
+ let stashed = false
400
+ try {
401
+ if (!clean) {
402
+ const sr = await git.stash("kkcode-auto-stash", cwd)
403
+ stashed = sr.ok
404
+ if (!stashed) {
405
+ // Stash failed — skip branch creation
406
+ gateStatus.git = { status: "warn", reason: "git stash failed" }
407
+ }
408
+ }
409
+ if (!stashed && !clean) {
410
+ // stash failed, skip branch creation (already set gateStatus above)
411
+ } else {
412
+ const created = await git.createBranch(branchName, cwd)
413
+ if (created.ok) {
414
+ gitBranch = branchName; gitActive = true
415
+ gateStatus.git = { status: "pass", branch: branchName, baseBranch: gitBaseBranch }
416
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED, sessionId, payload: { branch: branchName, baseBranch: gitBaseBranch } })
417
+ } else {
418
+ gateStatus.git = { status: "warn", reason: created.message }
419
+ }
420
+ }
421
+ } finally {
422
+ // Always restore stash on any exit path
423
+ if (stashed) await git.stashPop(cwd).catch(() => {})
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ // ========== H3: SCAFFOLD (脚手架) ==========
430
+ const scaffoldEnabled = longagentConfig.scaffold?.enabled !== false
431
+ if (scaffoldEnabled && stagePlan.stages.length > 0) {
432
+ await setPhase("H3", "scaffolding")
433
+ currentGate = "scaffold"
434
+ await syncState({ lastMessage: "H3: creating stub files" })
435
+
436
+ const scaffoldResult = await runScaffoldPhase({
437
+ objective: `${prompt}\n\n=== BLUEPRINT ARCHITECTURE ===\n${architectureText.slice(0, 4000)}`,
438
+ stagePlan, model, providerType, sessionId, configState,
439
+ baseUrl, apiKeyEnv, agent, signal, toolContext,
440
+ tddMode: hybridConfig.tdd_mode === true
441
+ })
442
+
443
+ gateStatus.scaffold = { status: scaffoldResult.scaffolded ? "pass" : "skip", fileCount: scaffoldResult.fileCount }
444
+ if (scaffoldResult.usage) accumulateUsage(scaffoldResult)
445
+ if (scaffoldResult.files?.length) {
446
+ fileChanges = mergeCappedFileChanges(fileChanges,
447
+ scaffoldResult.files.map(f => ({ path: f, addedLines: 0, removedLines: 0, stageId: "scaffold", taskId: "scaffold" })),
448
+ fileChangesLimit)
449
+ }
450
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_SCAFFOLD_COMPLETE, sessionId, payload: { fileCount: scaffoldResult.fileCount, files: scaffoldResult.files || [] } })
451
+ await syncState({ lastMessage: `H3: scaffolded ${scaffoldResult.fileCount} file(s)` })
452
+ }
453
+
454
+ // ========== H4+H5: CODING(并行) + DEBUGGING(回滚) 循环 ==========
455
+ const gatesConfig = longagentConfig.usability_gates || {}
456
+ let priorContext = [
457
+ "### Preview Findings", previewFindings.slice(0, 2000), "",
458
+ "### Blueprint Architecture", architectureText.slice(0, 3000)
459
+ ].join("\n")
460
+ const seenFilePaths = new Set() // #3 去重:跨阶段文件路径去重
461
+
462
+ let codingRollbackCount = 0
463
+ const maxCodingRollbacks = Number(hybridConfig.max_coding_rollbacks || 2)
464
+ const maxDebugIterations = Number(hybridConfig.debugging_max_iterations || 20)
465
+ let rerunCoding = true
466
+
467
+ while (rerunCoding && codingRollbackCount <= maxCodingRollbacks) {
468
+ rerunCoding = false
469
+
470
+ // --- H4: CODING (并行 stage 执行) ---
471
+ await setPhase("H4", "coding")
472
+ currentGate = "coding"
473
+ stageIndex = 0
474
+ const codingPhaseStart = Date.now()
475
+
476
+ while (stageIndex < stagePlan.stages.length) {
477
+ const state = await LongAgentManager.get(sessionId)
478
+ if (state?.stopRequested || signal?.aborted) break
479
+
480
+ // Phase 2: 阶段超时检测
481
+ if (Date.now() - codingPhaseStart > codingPhaseTimeoutMs) {
482
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT, sessionId, payload: { phase: "H4", elapsed: Date.now() - codingPhaseStart } })
483
+ if (degradationChain.canDegrade()) {
484
+ const degCtx = { model, taskProgress, configState, shouldStop: false }
485
+ const deg = degradationChain.apply(degCtx)
486
+ if (degCtx.model !== model) model = degCtx.model
487
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4" } })
488
+ if (deg.applied && deg.strategy === "graceful_stop") break
489
+ } else {
490
+ break
491
+ }
492
+ }
493
+
494
+ iteration++
495
+ const stage = stagePlan.stages[stageIndex]
496
+ currentGate = `stage:${stage.stageId}`
497
+ await syncState({ stageStatus: "running", lastMessage: `H4: running ${stage.stageId} (${stageIndex + 1}/${stagePlan.stages.length})` })
498
+
499
+ const seeded = Object.fromEntries(
500
+ stage.tasks.map(t => [t.taskId, taskProgress[t.taskId]]).filter(([, v]) => Boolean(v))
501
+ )
502
+
503
+ // #4 计划锚点 — 每阶段动态构建,不存入 priorContext 避免被压缩掉
504
+ const stageStatuses = stagePlan.stages.map((s, i) => {
505
+ const marker = i < stageIndex ? "✓" : i === stageIndex ? "→" : " "
506
+ return `[${marker}] 阶段${i + 1}: ${s.name || s.stageId}`
507
+ }).join("\n")
508
+ const planAnchor = `## 计划锚点\n目标: ${stagePlan.objective || prompt}\n进度: ${stageIndex + 1}/${stagePlan.stages.length}\n${stageStatuses}\n\n`
509
+
510
+ const stageResult = await runStageBarrier({
511
+ stage, sessionId, config: configState.config, model, providerType,
512
+ seedTaskProgress: seeded, objective: prompt,
513
+ stageIndex, stageCount: stagePlan.stages.length, priorContext: planAnchor + priorContext,
514
+ stuckTracker,
515
+ onTaskComplete: async (taskData) => {
516
+ await saveTaskCheckpoint(sessionId, taskData.stageId, taskData.taskId, taskData)
517
+ },
518
+ taskBus
519
+ })
520
+
521
+ // 合并结果
522
+ for (const [taskId, progress] of Object.entries(stageResult.taskProgress || {})) {
523
+ taskProgress[taskId] = { ...taskProgress[taskId], ...progress }
524
+ if (String(progress.lastReply || "").toLowerCase().includes("[task_complete]")) completionMarkerSeen = true
525
+ // #4 TaskBus: 解析 task 输出中的广播消息
526
+ if (taskBus && progress.lastReply) taskBus.parseTaskOutput(taskId, progress.lastReply)
527
+ // #3 动态重规划: 检测 [REPLAN:...] 标记
528
+ const replan = parseReplanMarker(progress.lastReply)
529
+ if (replan?.stages) {
530
+ const { plan, errors } = validateAndNormalizeStagePlan(replan, { objective: prompt, defaults: planDefaults })
531
+ if (!errors.length) {
532
+ stagePlan = plan
533
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_REPLAN, sessionId, payload: { newStageCount: plan.stages.length } })
534
+ }
535
+ }
536
+ }
537
+ if (stageResult.completionMarkerSeen) completionMarkerSeen = true
538
+ if (stageResult.fileChanges?.length) {
539
+ fileChanges = mergeCappedFileChanges(fileChanges, stageResult.fileChanges, fileChangesLimit)
540
+ }
541
+
542
+ gateStatus[stage.stageId] = {
543
+ status: stageResult.allSuccess ? "pass" : "fail",
544
+ successCount: stageResult.successCount, failCount: stageResult.failCount
545
+ }
546
+
547
+ // #1 阶段级压缩 + #3 文件去重 — 结构化摘要,跨阶段去重文件路径
548
+ const taskSummaries = Object.values(stageResult.taskProgress || {})
549
+ .filter(t => t.lastReply)
550
+ .map(t => ` - [${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 250)}`)
551
+ const stageFiles = (stageResult.fileChanges || [])
552
+ .map(f => (typeof f === "string" ? f : (f.path || f.file || "")))
553
+ .filter(Boolean)
554
+ const newFiles = stageFiles.filter(f => !seenFilePaths.has(f))
555
+ newFiles.forEach(f => seenFilePaths.add(f))
556
+ if (taskSummaries.length || newFiles.length) {
557
+ const fileNote = newFiles.length ? `\n 新增/修改文件: ${newFiles.join(", ")}` : ""
558
+ const failNote = !stageResult.allSuccess ? ` 失败任务数: ${stageResult.failCount}` : ""
559
+ priorContext += `\n### 阶段${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"}${failNote})\n${taskSummaries.join("\n")}${fileNote}\n`
560
+ }
561
+ // #4 TaskBus 注入到 priorContext
562
+ if (taskBus) {
563
+ const busCtx = taskBus.toContextString()
564
+ if (busCtx) priorContext += `\n${busCtx}\n`
565
+ }
566
+ // #13 上下文压缩
567
+ const pressureLimit = Number(hybridConfig.context_pressure_limit || 8000)
568
+ if (priorContext.length > pressureLimit) {
569
+ priorContext = await compressContext(priorContext, pressureLimit, { model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, toolContext })
570
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CONTEXT_COMPRESSED, sessionId, payload: { newLength: priorContext.length } })
571
+ }
572
+
573
+ lastProgress = {
574
+ percentage: Math.round(((stageIndex + (stageResult.allSuccess ? 1 : 0)) / Math.max(1, stagePlan.stages.length)) * 100),
575
+ currentStep: stageIndex + (stageResult.allSuccess ? 1 : 0),
576
+ totalSteps: stagePlan.stages.length
577
+ }
578
+
579
+ // Git: 每 stage 自动 commit
580
+ if (gitActive && stageResult.allSuccess && gitConfig.auto_commit_stages !== false) {
581
+ const msg = `[kkcode-hybrid] stage ${stage.stageId} completed (${stageIndex + 1}/${stagePlan.stages.length})`
582
+ await git.commitAll(msg, cwd)
583
+ }
584
+
585
+ // #10 增量门控:每个 stage 完成后运行轻量检查
586
+ if (hybridConfig.incremental_gates !== false && stageResult.allSuccess && stageIndex < stagePlan.stages.length - 1) {
587
+ const stageFiles = (stageResult.fileChanges || []).map(f => f.path).filter(Boolean)
588
+ if (stageFiles.length > 0) {
589
+ const miniGate = await runUsabilityGates({
590
+ sessionId, configState, model, providerType, baseUrl, apiKeyEnv, signal, toolContext,
591
+ objective: `Verify stage ${stage.stageId}: ${stage.name || ""}`, fileChanges: stageResult.fileChanges || [],
592
+ gatesConfig: { ...gatesConfig, lint: true, typecheck: true, test: false, security: false, build: false }, allowQuestion: false
593
+ })
594
+ if (miniGate.usage) accumulateUsage(miniGate)
595
+ gateStatus[`gate_${stage.stageId}`] = { status: miniGate.allPassed ? "pass" : "warn" }
596
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_INCREMENTAL_GATE, sessionId, payload: { stageId: stage.stageId, passed: miniGate.allPassed } })
597
+ // #18: Feed gate results into priorContext so subsequent stages see lint/typecheck feedback
598
+ if (!miniGate.allPassed && miniGate.failures?.length) {
599
+ const gateFeedback = miniGate.failures.slice(0, 3).map(f => `${f.gate}: ${(f.reason || "").slice(0, 150)}`).join("; ")
600
+ priorContext += `\n### Incremental Gate Warning (${stage.stageId})\n${gateFeedback}\n`
601
+ }
602
+ }
603
+ }
604
+
605
+ // #14 预算感知:检查 token 消耗是否超限
606
+ // #21: 增加基于历史平均值的预算预测
607
+ if (hybridConfig.budget_awareness !== false) {
608
+ const totalTokens = aggregateUsage.input + aggregateUsage.output
609
+ const budgetLimit = Number(longagentConfig.token_budget || 2000000)
610
+
611
+ // #21: Predict remaining budget based on average per-stage cost
612
+ const completedStages = stageIndex + (stageResult.allSuccess ? 1 : 0)
613
+ const remainingStages = stagePlan.stages.length - completedStages
614
+ if (completedStages > 0 && remainingStages > 0) {
615
+ const avgPerStage = totalTokens / completedStages
616
+ const predicted = totalTokens + avgPerStage * remainingStages
617
+ if (predicted > budgetLimit && totalTokens <= budgetLimit * 0.9) {
618
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING, sessionId, payload: { totalTokens, budgetLimit, predicted: Math.round(predicted), percentage: Math.round(totalTokens / budgetLimit * 100), forecast: true } })
619
+ await syncState({ lastMessage: `H4: budget forecast — predicted ${Math.round(predicted / 1000)}k tokens (limit ${Math.round(budgetLimit / 1000)}k)` })
620
+ }
621
+ }
622
+
623
+ if (totalTokens > budgetLimit * 0.9) {
624
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING, sessionId, payload: { totalTokens, budgetLimit, percentage: Math.round(totalTokens / budgetLimit * 100) } })
625
+ await syncState({ lastMessage: `H4: budget warning — ${Math.round(totalTokens / budgetLimit * 100)}% used` })
626
+ }
627
+ if (totalTokens > budgetLimit) {
628
+ // Phase 6: 尝试降级而非直接 break
629
+ if (degradationChain.canDegrade()) {
630
+ const degCtx2 = { model, taskProgress, configState, shouldStop: false }
631
+ const deg = degradationChain.apply(degCtx2)
632
+ if (degCtx2.model !== model) model = degCtx2.model
633
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4", reason: "budget_exceeded" } })
634
+ if (deg.applied && deg.strategy === "graceful_stop") {
635
+ await syncState({ status: "budget_exceeded", lastMessage: `H4: budget exceeded, graceful stop` })
636
+ break
637
+ }
638
+ } else {
639
+ await syncState({ status: "budget_exceeded", lastMessage: `H4: budget exceeded (${totalTokens}/${budgetLimit})` })
640
+ break
641
+ }
642
+ }
643
+ }
644
+
645
+ if (!stageResult.allSuccess) {
646
+ recoveryCount++
647
+ const backoffMs = Math.min(1000 * 2 ** (recoveryCount - 1), 30000)
648
+ await new Promise(r => setTimeout(r, backoffMs))
649
+ const maxStageRecoveries = Number(longagentConfig.max_stage_recoveries ?? 3)
650
+ if (recoveryCount >= maxStageRecoveries) {
651
+ // Phase 6: 尝试降级而非直接 abort
652
+ if (degradationChain.canDegrade()) {
653
+ const degCtx3 = { model, taskProgress, configState, shouldStop: false }
654
+ const deg = degradationChain.apply(degCtx3)
655
+ if (degCtx3.model !== model) model = degCtx3.model
656
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4", reason: "max_recoveries" } })
657
+ if (deg.applied && deg.strategy === "graceful_stop") {
658
+ await syncState({ status: "error", lastMessage: `stage ${stage.stageId} aborted after degradation` })
659
+ break
660
+ }
661
+ // 降级成功但非 graceful_stop,重置 recoveryCount 继续
662
+ recoveryCount = 0
663
+ } else {
664
+ await syncState({ status: "error", lastMessage: `stage ${stage.stageId} aborted after ${recoveryCount} recoveries` })
665
+ break
666
+ }
667
+ }
668
+ // Phase 1: 根据错误类别决定是否重试
669
+ for (const [taskId, tp] of Object.entries(taskProgress)) {
670
+ if (tp.status === "error") {
671
+ const category = classifyError(tp.lastError)
672
+ if (category === ERROR_CATEGORIES.PERMANENT || category === ERROR_CATEGORIES.UNKNOWN) {
673
+ taskProgress[taskId] = { ...tp, status: "error", skipReason: `${category} error` }
674
+ } else {
675
+ taskProgress[taskId] = { ...tp, status: "retrying", attempt: 0 }
676
+ }
677
+ }
678
+ }
679
+ continue
680
+ }
681
+
682
+ stageIndex++
683
+ recoveryCount = 0 // reset per-stage recovery counter after successful stage
684
+ await saveCheckpoint(sessionId, { name: `hybrid_stage_${stage.stageId}`, iteration, currentPhase, stageIndex, stagePlan, taskProgress, planFrozen, lastProgress })
685
+ }
686
+
687
+ // #11 Cross-review:H4 完成后、H5 之前,让独立 agent 审查代码
688
+ if (hybridConfig.cross_review !== false && fileChanges.length > 0) {
689
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CROSS_REVIEW, sessionId, payload: { fileCount: fileChanges.length } })
690
+ const reviewFiles = fileChanges.slice(0, 20).map(f => f.path).join(", ")
691
+ const reviewOut = await processTurnLoop({
692
+ prompt: [
693
+ "You are the CROSS-REVIEW agent. Multiple parallel sub-agents just completed their coding tasks independently.",
694
+ "Your job: verify that their outputs are compatible, correct, and integrate properly.",
695
+ "",
696
+ "## Files to review:",
697
+ reviewFiles,
698
+ "",
699
+ "## Review Checklist",
700
+ "1. IMPORT RESOLUTION: Do all cross-file imports resolve? Are exported symbols correct?",
701
+ "2. INTERFACE COMPATIBILITY: Do function signatures match what callers expect?",
702
+ "3. ERROR HANDLING: Are errors properly caught, propagated, or thrown? No silent failures?",
703
+ "4. RESOURCE CLEANUP: Are timers cleared, listeners removed, handles closed in all code paths?",
704
+ "5. EDGE CASES: Null/undefined checks, empty arrays, concurrent access guards?",
705
+ "6. CONSISTENCY: Same naming conventions, error patterns, async style across files?",
706
+ "",
707
+ `## Original Objective: ${prompt}`,
708
+ "",
709
+ "## Output Format",
710
+ "For each issue found, output: [FAILED_TASK: taskId] with a description of the problem.",
711
+ "If no issues found, state that the cross-review passed.",
712
+ "Focus on REAL bugs that would cause runtime failures — not style preferences."
713
+ ].join("\n"),
714
+ mode: "agent", agent: getAgent("debugging-agent"),
715
+ model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion: false, toolContext
716
+ })
717
+ accumulateUsage(reviewOut)
718
+ // 将审查发现注入 priorContext
719
+ if (reviewOut.reply) priorContext += `\n### Cross-Review Findings\n${reviewOut.reply.slice(0, 1500)}\n`
720
+ }
721
+
722
+ // --- H5: DEBUGGING (回滚检测) ---
723
+ await setPhase("H5", "debugging")
724
+ currentGate = "debugging"
725
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_START, sessionId, payload: { codingRollbackCount } })
726
+ await syncState({ lastMessage: "H5: debugging agent verifying implementation" })
727
+
728
+ const debugModel = getModelForStage("debugging")
729
+ const debugPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.DEBUGGING, {
730
+ preview: previewFindings.slice(0, 2000),
731
+ blueprint: architectureText.slice(0, 3000),
732
+ coding: priorContext.slice(0, 4000)
733
+ }, prompt)
734
+
735
+ let debugIter = 0
736
+ let debugDone = false
737
+ const semanticTracker = createSemanticErrorTracker(3)
738
+ const debugPhaseStart = Date.now()
739
+
740
+ while (!debugDone && debugIter < maxDebugIterations) {
741
+ debugIter++
742
+ iteration++
743
+ const state = await LongAgentManager.get(sessionId)
744
+ if (state?.stopRequested || signal?.aborted) break
745
+
746
+ // Phase 2: debugging 阶段超时检测
747
+ if (Date.now() - debugPhaseStart > debuggingPhaseTimeoutMs) {
748
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT, sessionId, payload: { phase: "H5", elapsed: Date.now() - debugPhaseStart } })
749
+ if (degradationChain.canDegrade()) {
750
+ const degCtx4 = { model, taskProgress, configState, shouldStop: false }
751
+ const deg = degradationChain.apply(degCtx4)
752
+ if (degCtx4.model !== model) model = degCtx4.model
753
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H5" } })
754
+ if (deg.applied && deg.strategy === "graceful_stop") break
755
+ } else {
756
+ break
757
+ }
758
+ }
759
+
760
+ const debugOut = await processTurnLoop({
761
+ prompt: debugPrompt, mode: "agent", agent: getAgent("debugging-agent"),
762
+ model: debugModel.model, providerType: debugModel.providerType,
763
+ sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
764
+ })
765
+ accumulateUsage(debugOut)
766
+ finalReply = debugOut.reply || ""
767
+
768
+ // 防卡死检测
769
+ if (debugOut.toolEvents?.length) {
770
+ const stuckResult = stuckTracker.track(debugOut.toolEvents)
771
+ if (stuckResult.isStuck) {
772
+ stuckTracker.resetReadOnlyCount()
773
+ await EventBus.emit({
774
+ type: EVENT_TYPES.LONGAGENT_ALERT, sessionId,
775
+ payload: { kind: "stuck_warning", stage: "H5:debugging", reason: stuckResult.reason, debugIter }
776
+ })
777
+ await syncState({ lastMessage: `H5: stuck detected (${stuckResult.reason}), iter ${debugIter}` })
778
+ }
779
+ }
780
+
781
+ // Phase 5: 语义级错误检测
782
+ const semResult = semanticTracker.track(finalReply)
783
+ if (semResult.isRepeated) {
784
+ await EventBus.emit({
785
+ type: EVENT_TYPES.LONGAGENT_SEMANTIC_ERROR_REPEATED, sessionId,
786
+ payload: { error: semResult.error, count: semResult.count, debugIter }
787
+ })
788
+ // 注入更详细的错误分析提示,避免无限循环
789
+ await syncState({ lastMessage: `H5: repeated error detected (${semResult.count}x): ${(semResult.error || "").slice(0, 80)}` })
790
+ }
791
+
792
+ if (detectStageComplete(finalReply, LONGAGENT_4STAGE_STAGES.DEBUGGING)) {
793
+ debugDone = true
794
+ gateStatus.debugging = { status: "pass", iterations: debugIter }
795
+ }
796
+
797
+ if (detectReturnToCoding(finalReply)) {
798
+ codingRollbackCount++
799
+ rerunCoding = true
800
+ // #1 细粒度回滚:优先只重置被标记的失败 task
801
+ const failedIds = extractFailedTaskIds(finalReply)
802
+ if (failedIds.length > 0) {
803
+ for (const fid of failedIds) {
804
+ if (taskProgress[fid]) taskProgress[fid] = { ...taskProgress[fid], status: "retrying", attempt: 0 }
805
+ }
806
+ } else {
807
+ // 回退:重置所有 error 状态的 task
808
+ for (const [taskId, tp] of Object.entries(taskProgress)) {
809
+ if (tp.status === "error") taskProgress[taskId] = { ...tp, status: "retrying", attempt: 0 }
810
+ }
811
+ }
812
+ gateStatus.debugging = { status: "rollback", iterations: debugIter, rollbackCount: codingRollbackCount, failedTaskIds: failedIds }
813
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_RETURN_TO_CODING, sessionId, payload: { rollbackCount: codingRollbackCount, failedTaskIds: failedIds } })
814
+ break
815
+ }
816
+
817
+ if (/\[TASK_COMPLETE\]/i.test(finalReply)) { completionMarkerSeen = true; debugDone = true }
818
+ await syncState({ lastMessage: `H5: debugging iteration ${debugIter}/${maxDebugIterations}` })
819
+ }
820
+
821
+ if (!debugDone && !rerunCoding) {
822
+ gateStatus.debugging = { status: "timeout", iterations: debugIter }
823
+ }
824
+
825
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_COMPLETE, sessionId, payload: { debugIter, rollback: rerunCoding } })
826
+ await syncState({ lastMessage: rerunCoding ? `H5: rollback to coding (attempt ${codingRollbackCount})` : `H5: debugging complete` })
827
+ } // end while(rerunCoding)
828
+
829
+ // ========== H5.5: COMPLETION VALIDATION ==========
830
+ if (hybridConfig.completion_validation !== false) {
831
+ await setPhase("H5.5", "completion_validation")
832
+ await syncState({ lastMessage: "H5.5: validating completion" })
833
+
834
+ const cwd = process.cwd()
835
+ try {
836
+ const validator = await createValidator({ cwd, configState })
837
+ const report = await validator.validate({ todoState: toolContext?._todoState, level: "standard" })
838
+ gateStatus.completionValidation = {
839
+ status: report.verdict === "BLOCK" ? "fail" : "pass",
840
+ verdict: report.verdict,
841
+ failedChecks: report.results?.filter(r => !r.passed).length || 0
842
+ }
843
+
844
+ if (report.verdict === "BLOCK" && !completionMarkerSeen) {
845
+ const fixPrompt = [
846
+ "## Completion Validation Failed — Fix Required",
847
+ "",
848
+ `Original objective: ${prompt}`,
849
+ "",
850
+ "## Validation Issues Found:",
851
+ report.message,
852
+ "",
853
+ "## Fix Instructions",
854
+ "1. Read each failing check and identify the root cause",
855
+ "2. Fix the issue in the source code (not by suppressing the check)",
856
+ "3. Re-run the relevant verification command to confirm the fix",
857
+ "4. If a fix requires changes to multiple files, ensure cross-file consistency",
858
+ "",
859
+ "When ALL issues are resolved and verified, include [TASK_COMPLETE] in your response."
860
+ ].join("\n")
861
+ const fixOut = await processTurnLoop({
862
+ prompt: fixPrompt, mode: "agent", agent: getAgent("coding-agent"),
863
+ model, providerType, sessionId, configState,
864
+ baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
865
+ })
866
+ accumulateUsage(fixOut)
867
+ iteration++
868
+ if (/\[TASK_COMPLETE\]/i.test(fixOut.reply || "")) completionMarkerSeen = true
869
+ finalReply = fixOut.reply || finalReply
870
+ }
871
+ } catch (valErr) {
872
+ gateStatus.completionValidation = { status: "warn", reason: `skipped: ${valErr.message}` }
873
+ }
874
+ }
875
+
876
+ // ========== H6: USABILITY GATES ==========
877
+ await setPhase("H6", "gates")
878
+ currentGate = "gates"
879
+ await syncState({ lastMessage: "H6: running usability gates" })
880
+
881
+ // Gate 偏好提示(首次运行时询问用户)
882
+ const shouldPromptGates = gatesConfig.prompt_user === "first_run" || gatesConfig.prompt_user === "always"
883
+ if (shouldPromptGates && allowQuestion) {
884
+ const hasPrefs = await hasGatePreferences()
885
+ if (!hasPrefs || gatesConfig.prompt_user === "always") {
886
+ const gateAskResult = await processTurnLoop({
887
+ prompt: buildGatePromptText(),
888
+ mode: "ask", model, providerType, sessionId, configState,
889
+ baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
890
+ })
891
+ accumulateUsage(gateAskResult)
892
+ const gatePrefs = parseGateSelection(gateAskResult.reply)
893
+ await saveGatePreferences(gatePrefs)
894
+ for (const [gate, enabled] of Object.entries(gatePrefs)) {
895
+ if (configState.config.agent.longagent.usability_gates[gate]) {
896
+ configState.config.agent.longagent.usability_gates[gate].enabled = enabled
897
+ }
898
+ }
899
+ } else {
900
+ const savedPrefs = await getGatePreferences()
901
+ if (savedPrefs) {
902
+ for (const [gate, enabled] of Object.entries(savedPrefs)) {
903
+ if (configState.config.agent.longagent.usability_gates[gate]) {
904
+ configState.config.agent.longagent.usability_gates[gate].enabled = enabled
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }
910
+
911
+ let gateAttempt = 0
912
+
913
+ while (gateAttempt < maxGateAttempts) {
914
+ gateAttempt++
915
+ const state = await LongAgentManager.get(sessionId)
916
+ if (state?.stopRequested || signal?.aborted) break
917
+
918
+ const gateResult = await runUsabilityGates({
919
+ sessionId, configState, model, providerType,
920
+ baseUrl, apiKeyEnv, signal, toolContext,
921
+ objective: prompt, fileChanges,
922
+ gatesConfig, allowQuestion
923
+ })
924
+ if (gateResult.usage) accumulateUsage(gateResult)
925
+
926
+ if (gateResult.allPassed) {
927
+ gateStatus.usabilityGates = { status: "pass", attempt: gateAttempt }
928
+ break
929
+ }
930
+
931
+ lastGateFailures = gateResult.failures || []
932
+ gateStatus.usabilityGates = { status: "fixing", attempt: gateAttempt, failures: summarizeGateFailures(lastGateFailures) }
933
+ await syncState({ lastMessage: `H6: gate failures (attempt ${gateAttempt}/${maxGateAttempts}), fixing...` })
934
+
935
+ // 修复循环:根据 gate 类型选择修复策略 (Phase 8)
936
+ const strategy = getGateFixStrategy(lastGateFailures)
937
+
938
+ // lint 失败时先尝试自动修复
939
+ if (strategy.autoFix) {
940
+ try {
941
+ const { execSync } = await import("node:child_process")
942
+ execSync(strategy.autoFix, { cwd: process.cwd(), timeout: 30000, stdio: "ignore" })
943
+ } catch { /* autofix failed, fall through to agent */ }
944
+ }
945
+
946
+ const gateFailureSummary = summarizeGateFailures(lastGateFailures)
947
+ const fixPrompt = [
948
+ `## Quality Gate Failures — Attempt ${gateAttempt}/${maxGateAttempts}`,
949
+ "",
950
+ `${strategy.prefix || "Fix the following quality gate failures:"}`,
951
+ "",
952
+ gateFailureSummary,
953
+ "",
954
+ "## Fix Protocol",
955
+ "1. Read the error output carefully — identify the ROOT CAUSE, not just the symptom",
956
+ "2. Fix the source code (do NOT disable or skip the gate check)",
957
+ "3. Re-run the failing command to verify the fix works",
958
+ "4. If the fix touches shared code, verify no regressions in other modules",
959
+ "",
960
+ `Original objective: ${prompt}`
961
+ ].join("\n")
962
+ const fixOut = await processTurnLoop({
963
+ prompt: fixPrompt, mode: "agent", agent: getAgent(strategy.agent || "coding-agent"),
964
+ model, providerType, sessionId, configState,
965
+ baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
966
+ })
967
+ accumulateUsage(fixOut)
968
+ iteration++
969
+ }
970
+
971
+ if (gateAttempt >= maxGateAttempts && lastGateFailures.length) {
972
+ gateStatus.usabilityGates = { status: "fail", attempt: gateAttempt, failures: summarizeGateFailures(lastGateFailures) }
973
+ }
974
+
975
+ // ========== H7: GIT MERGE ==========
976
+ if (gitActive && gitBaseBranch && gitBranch) {
977
+ await setPhase("H7", "git_merge")
978
+ try {
979
+ await git.commitAll(`[kkcode-hybrid] session ${sessionId} completed`, cwd)
980
+ if (gitConfig.auto_merge !== false) {
981
+ await git.checkoutBranch(gitBaseBranch, cwd)
982
+ await git.mergeBranch(gitBranch, cwd)
983
+ await git.deleteBranch(gitBranch, cwd)
984
+ gateStatus.gitMerge = { status: "pass", branch: gitBranch, baseBranch: gitBaseBranch }
985
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_MERGED, sessionId, payload: { branch: gitBranch, baseBranch: gitBaseBranch } })
986
+ }
987
+ } catch (err) {
988
+ // Phase 9: 自愈式 Git 操作
989
+ if (git.isConflictError(err)) {
990
+ try {
991
+ const conflictFiles = await git.getConflictFiles(cwd)
992
+ if (conflictFiles.length > 0) {
993
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_CONFLICT_RESOLUTION, sessionId, payload: { files: conflictFiles } })
994
+ const conflictPrompt = [
995
+ "## Git Merge Conflict Resolution",
996
+ "",
997
+ "The following files have merge conflicts that must be resolved:",
998
+ ...conflictFiles.map(f => `- ${f}`),
999
+ "",
1000
+ "## Resolution Protocol",
1001
+ "1. Read each conflicted file and locate ALL conflict markers (<<<<<<< ======= >>>>>>>)",
1002
+ "2. For each conflict block:",
1003
+ " - Understand what BOTH sides intended (ours = feature branch, theirs = base branch)",
1004
+ " - Keep the feature branch changes (our work) unless they break base branch functionality",
1005
+ " - If both sides modified the same logic, merge them intelligently (not just pick one)",
1006
+ " - Remove ALL conflict markers — no <<<<<<< or ======= or >>>>>>> should remain",
1007
+ "3. After resolving, run syntax check on each file (node --check / python -m py_compile)",
1008
+ "4. Verify imports still resolve correctly across resolved files"
1009
+ ].join("\n")
1010
+ const conflictOut = await processTurnLoop({
1011
+ prompt: conflictPrompt, mode: "agent", agent: getAgent("coding-agent"),
1012
+ model, providerType, sessionId, configState,
1013
+ baseUrl, apiKeyEnv, signal, output, allowQuestion: false, toolContext
1014
+ })
1015
+ accumulateUsage(conflictOut)
1016
+ const commitResult = await git.commitAll(`[kkcode-hybrid] resolved merge conflicts`, cwd)
1017
+ if (commitResult.ok) {
1018
+ gateStatus.gitMerge = { status: "pass", branch: gitBranch, baseBranch: gitBaseBranch, conflictsResolved: true }
1019
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_MERGED, sessionId, payload: { branch: gitBranch, baseBranch: gitBaseBranch } })
1020
+ } else {
1021
+ await git.mergeAbort(cwd)
1022
+ gateStatus.gitMerge = { status: "warn", reason: "conflict resolution failed, staying on feature branch" }
1023
+ }
1024
+ } else {
1025
+ gateStatus.gitMerge = { status: "warn", reason: err.message }
1026
+ }
1027
+ } catch (resolveErr) {
1028
+ await git.mergeAbort(cwd).catch(() => {})
1029
+ gateStatus.gitMerge = { status: "warn", reason: `conflict resolution error: ${resolveErr.message}` }
1030
+ }
1031
+ } else {
1032
+ gateStatus.gitMerge = { status: "warn", reason: err.message }
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ // #5 保存 project memory
1038
+ if (hybridConfig.project_memory !== false && previewFindings) {
1039
+ try {
1040
+ const newMemory = parseMemoryFromPreview(previewFindings)
1041
+ if (newMemory.techStack.length) {
1042
+ const merged = { ...projectMemory, techStack: [...new Set([...(projectMemory?.techStack || []), ...newMemory.techStack])].slice(0, 20), patterns: [...new Set([...(projectMemory?.patterns || []), ...newMemory.patterns])].slice(0, 20), conventions: projectMemory?.conventions || [] }
1043
+ await saveProjectMemory(cwd, merged)
1044
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_SAVED, sessionId, payload: { techStackCount: merged.techStack.length } })
1045
+ }
1046
+ } catch { /* ignore memory save errors */ }
1047
+ }
1048
+
1049
+ // Phase 10: Checkpoint 清理
1050
+ if (hybridConfig.checkpoint_cleanup !== false) {
1051
+ try {
1052
+ const cleanResult = await cleanupCheckpoints(sessionId, {
1053
+ maxKeep: Number(hybridConfig.checkpoint_max_keep || 10),
1054
+ keepStageCheckpoints: true
1055
+ })
1056
+ if (cleanResult.removed > 0) {
1057
+ await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_CHECKPOINT_CLEANED, sessionId, payload: { removed: cleanResult.removed } })
1058
+ }
1059
+ } catch { /* ignore cleanup errors */ }
1060
+ }
1061
+
1062
+ // ========== 完成 ==========
1063
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
1064
+ const finalStatus = completionMarkerSeen ? "completed" : "done"
1065
+ await LongAgentManager.update(sessionId, { status: finalStatus, lastMessage: "hybrid longagent complete", elapsed })
1066
+ await markSessionStatus(sessionId, finalStatus === "completed" ? "completed" : "active")
1067
+
1068
+ const stats = stageProgressStats(taskProgress)
1069
+
1070
+ // Phase 11: 恢复建议生成
1071
+ let recoverySuggestions = null
1072
+ if (finalStatus !== "completed") {
1073
+ recoverySuggestions = generateRecoverySuggestions({
1074
+ status: finalStatus,
1075
+ taskProgress,
1076
+ gateStatus,
1077
+ phase: currentPhase,
1078
+ recoveryCount,
1079
+ fileChanges
1080
+ })
1081
+ }
1082
+
1083
+ return {
1084
+ sessionId, turnId: `turn_long_${Date.now()}`,
1085
+ reply: finalReply || "hybrid longagent complete",
1086
+ usage: aggregateUsage, toolEvents, iterations: iteration,
1087
+ status: finalStatus, phase: currentPhase,
1088
+ gateStatus, currentGate, lastGateFailures, recoveryCount,
1089
+ progress: lastProgress, elapsed,
1090
+ stageIndex, stageCount: stagePlan?.stages?.length || 0,
1091
+ planFrozen, taskProgress, fileChanges,
1092
+ stageProgress: { done: stats.done, total: stats.total },
1093
+ remainingFilesCount: stats.remainingFilesCount,
1094
+ gitBranch, gitBaseBranch,
1095
+ recoverySuggestions
1096
+ }
1097
+ }