@yuaone/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,1555 @@
1
+ /**
2
+ * @module execution-engine
3
+ * @description Top-level Execution Engine — StateMachine(brain) ↔ AgentLoop(hands) 오케스트레이터.
4
+ *
5
+ * 모든 하위 시스템을 연결하여 사용자 목표를 end-to-end로 실행한다:
6
+ * - **StateMachine** — phase 전이 (analyze → plan → implement → verify → done)
7
+ * - **AgentLoop** — LLM ↔ Tool 반복 실행 (implement/fix phase)
8
+ * - **HierarchicalPlanner** — 3-level 계획 수립 (plan phase)
9
+ * - **SelfReflection** — 6-dimension 심층 검증 (verify phase)
10
+ * - **CodebaseContext** — 코드베이스 인덱싱/검색 (analyze phase)
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const engine = new ExecutionEngine({
15
+ * byokConfig: { provider: "anthropic", apiKey: "sk-..." },
16
+ * projectPath: "/my/project",
17
+ * toolExecutor: myExecutor,
18
+ * maxIterations: 100,
19
+ * totalTokenBudget: 500_000,
20
+ * });
21
+ *
22
+ * engine.on("phase:enter", (phase) => console.log(`Phase: ${phase}`));
23
+ * engine.on("monologue", (entry) => console.log(entry.thought));
24
+ *
25
+ * const result = await engine.execute("Add error handling to all API routes");
26
+ * console.log(result.success, result.summary);
27
+ * ```
28
+ */
29
+ import { EventEmitter } from "node:events";
30
+ import { readFile } from "node:fs/promises";
31
+ import { BYOKClient } from "./llm-client.js";
32
+ import { AgentLoop } from "./agent-loop.js";
33
+ import { AgentLogger } from "./agent-logger.js";
34
+ import { AgentStateMachine, } from "./state-machine.js";
35
+ import { CodebaseContext } from "./codebase-context.js";
36
+ import { VectorIndex } from "./vector-index.js";
37
+ import { LanguageSupport } from "./language-support.js";
38
+ import { HierarchicalPlanner, } from "./hierarchical-planner.js";
39
+ import { SelfReflection, } from "./self-reflection.js";
40
+ import { PlanGraphManager } from "./kernel.js";
41
+ import { MCPClient } from "./mcp-client.js";
42
+ import { PerfOptimizer } from "./perf-optimizer.js";
43
+ import { SandboxManager } from "./sandbox-tiers.js";
44
+ import { DebateOrchestrator } from "./debate-orchestrator.js";
45
+ import { SecurityScanner } from "./security-scanner.js";
46
+ import { DocIntelligence } from "./doc-intelligence.js";
47
+ import { IntentInferenceEngine } from "./intent-inference.js";
48
+ import { SpeculativeExecutor } from "./speculative-executor.js";
49
+ // ─── Defaults ────────────────────────────────────────────────────
50
+ const ENGINE_DEFAULTS = {
51
+ maxIterations: 100,
52
+ totalTokenBudget: 500_000,
53
+ maxFixAttempts: 3,
54
+ enableCodebaseIndex: true,
55
+ enableHierarchicalPlanning: true,
56
+ enableDeepVerify: true,
57
+ enableMonologue: true,
58
+ enableLearning: true,
59
+ skipDesignForSimple: true,
60
+ enableParallel: true,
61
+ enableParallelExecution: true,
62
+ maxParallelAgents: 3,
63
+ };
64
+ // ─── ExecutionEngine ─────────────────────────────────────────────
65
+ /**
66
+ * ExecutionEngine — YUAN 에이전트의 최상위 오케스트레이터.
67
+ *
68
+ * StateMachine이 phase 전이를 결정하고,
69
+ * ExecutionEngine이 각 phase에서 적절한 하위 시스템을 호출한다.
70
+ *
71
+ * - **analyze**: CodebaseContext로 프로젝트 구조 파악
72
+ * - **design**: LLM으로 접근법 제안
73
+ * - **plan**: HierarchicalPlanner로 3-level 계획 수립
74
+ * - **implement**: AgentLoop로 LLM ↔ Tool 반복 실행
75
+ * - **verify**: SelfReflection.deepVerify로 6-dimension 검증
76
+ * - **fix**: AgentLoop로 에러 수정
77
+ * - **replan**: HierarchicalPlanner.replan으로 재계획
78
+ */
79
+ export class ExecutionEngine extends EventEmitter {
80
+ config;
81
+ llmClient;
82
+ reflection;
83
+ codebaseContext;
84
+ vectorIndex;
85
+ stateMachine;
86
+ abortController;
87
+ changedFiles;
88
+ originalFiles;
89
+ lastTermination;
90
+ lastVerifyResult;
91
+ hierarchicalPlan;
92
+ planGraph;
93
+ sessionId;
94
+ _logger;
95
+ sessionPersistence;
96
+ /** DAG 기반 병렬 실행 결과 캐시 (stepIndex → StepResult) */
97
+ parallelStepResults;
98
+ /** DAG 기반 병렬 실행이 완료되었는지 */
99
+ parallelExecutionDone;
100
+ /** MCP server configurations (stored from constructor config) */
101
+ mcpServerConfigs;
102
+ /** MCP Client — external tool bridge (null if disabled) */
103
+ mcpClient = null;
104
+ /** MCP tool definitions discovered at runtime */
105
+ mcpToolDefinitions = [];
106
+ /** Performance optimizer (null if disabled) */
107
+ perfOptimizer = null;
108
+ /** Sandbox manager for tool call validation (null if disabled) */
109
+ sandboxManager = null;
110
+ constructor(config) {
111
+ super();
112
+ this.config = {
113
+ byokConfig: config.byokConfig,
114
+ projectPath: config.projectPath,
115
+ toolExecutor: config.toolExecutor,
116
+ maxIterations: config.maxIterations ?? ENGINE_DEFAULTS.maxIterations,
117
+ totalTokenBudget: config.totalTokenBudget ?? ENGINE_DEFAULTS.totalTokenBudget,
118
+ maxFixAttempts: config.maxFixAttempts ?? ENGINE_DEFAULTS.maxFixAttempts,
119
+ enableCodebaseIndex: config.enableCodebaseIndex ?? ENGINE_DEFAULTS.enableCodebaseIndex,
120
+ enableHierarchicalPlanning: config.enableHierarchicalPlanning ?? ENGINE_DEFAULTS.enableHierarchicalPlanning,
121
+ enableDeepVerify: config.enableDeepVerify ?? ENGINE_DEFAULTS.enableDeepVerify,
122
+ enableMonologue: config.enableMonologue ?? ENGINE_DEFAULTS.enableMonologue,
123
+ enableLearning: config.enableLearning ?? ENGINE_DEFAULTS.enableLearning,
124
+ skipDesignForSimple: config.skipDesignForSimple ?? ENGINE_DEFAULTS.skipDesignForSimple,
125
+ enableParallel: config.enableParallel ?? ENGINE_DEFAULTS.enableParallel,
126
+ enableParallelExecution: config.enableParallelExecution ?? ENGINE_DEFAULTS.enableParallelExecution,
127
+ maxParallelAgents: config.maxParallelAgents ?? ENGINE_DEFAULTS.maxParallelAgents,
128
+ enableVectorSearch: config.enableVectorSearch ?? false,
129
+ enableDebate: config.enableDebate ?? false,
130
+ enableSecurityScan: config.enableSecurityScan ?? false,
131
+ enableDocGeneration: config.enableDocGeneration ?? false,
132
+ enableIntentInference: config.enableIntentInference ?? true,
133
+ enableSpeculative: config.enableSpeculative ?? false,
134
+ speculativeMaxApproaches: config.speculativeMaxApproaches ?? 3,
135
+ governorConfig: config.governorConfig,
136
+ contextConfig: config.contextConfig,
137
+ approvalConfig: config.approvalConfig,
138
+ approvalHandler: config.approvalHandler,
139
+ autoFixConfig: config.autoFixConfig,
140
+ };
141
+ this.llmClient = new BYOKClient(this.config.byokConfig);
142
+ this.sessionId = config.sessionId ?? `engine-${Date.now().toString(36)}`;
143
+ this.sessionPersistence = config.sessionPersistence ?? null;
144
+ this.reflection = new SelfReflection(this.sessionId, {
145
+ enableDeepVerify: this.config.enableDeepVerify,
146
+ enableMonologue: this.config.enableMonologue,
147
+ enableLearning: this.config.enableLearning,
148
+ });
149
+ // Initialize structured logger
150
+ this._logger = config.logger ?? new AgentLogger({
151
+ sessionId: this.sessionId,
152
+ level: "info",
153
+ outputs: [{ type: "memory" }],
154
+ ...config.loggerConfig,
155
+ });
156
+ this.codebaseContext = null;
157
+ this.vectorIndex = null;
158
+ this.stateMachine = null;
159
+ this.planGraph = null;
160
+ this.abortController = new AbortController();
161
+ this.changedFiles = new Set();
162
+ this.originalFiles = new Map();
163
+ this.lastTermination = { reason: "USER_CANCELLED" };
164
+ this.parallelStepResults = new Map();
165
+ this.parallelExecutionDone = false;
166
+ // Store MCP server configs for use in execute()
167
+ this.mcpServerConfigs = config.mcpServerConfigs ?? [];
168
+ // Initialize PerfOptimizer if enabled
169
+ if (config.enablePerfTracking) {
170
+ this.perfOptimizer = new PerfOptimizer();
171
+ }
172
+ // Initialize SandboxManager if enabled
173
+ if (config.enableSandbox) {
174
+ this.sandboxManager = new SandboxManager({
175
+ projectPath: this.config.projectPath,
176
+ defaultTier: config.sandboxTier,
177
+ });
178
+ }
179
+ // Wire reflection monologue events to engine events
180
+ this.reflection.on("monologue:entry", (entry) => {
181
+ this.emit("monologue", entry);
182
+ });
183
+ }
184
+ // ─── Logger Access ─────────────────────────────────────────────
185
+ /** 실행 로거 — 실행 후 로그 조회에 사용 */
186
+ get logger() {
187
+ return this._logger;
188
+ }
189
+ /** 현재 PlanGraphManager 인스턴스 (계획 진행 상태 조회용) */
190
+ get currentPlanGraph() {
191
+ return this.planGraph;
192
+ }
193
+ /**
194
+ * 현재 계획의 진행 상황을 반환한다.
195
+ * PlanGraphManager가 없으면 null을 반환한다.
196
+ *
197
+ * @returns 진행 상황 객체 또는 null
198
+ */
199
+ getPlanProgress() {
200
+ if (!this.planGraph)
201
+ return null;
202
+ const progress = this.planGraph.getProgress();
203
+ const graphState = this.planGraph.getState();
204
+ return {
205
+ completed: progress.completed,
206
+ total: progress.total,
207
+ percent: progress.percent,
208
+ running: [...graphState.runningNodes],
209
+ failed: [...graphState.failedNodes],
210
+ };
211
+ }
212
+ // ─── Main Entry ────────────────────────────────────────────────
213
+ /**
214
+ * 사용자 목표를 end-to-end로 실행한다.
215
+ *
216
+ * 내부적으로 StateMachine을 생성하고, 각 phase에 대한 콜백을 연결하여
217
+ * idle → analyze → plan → implement → verify → done까지 자동 진행한다.
218
+ *
219
+ * @param goal - 사용자의 실행 목표
220
+ * @returns 실행 결과 (성공 여부, 변경 파일, 토큰 사용량 등)
221
+ */
222
+ async execute(goal) {
223
+ const startTime = Date.now();
224
+ this.abortController = new AbortController();
225
+ this.changedFiles.clear();
226
+ this.originalFiles.clear();
227
+ this.lastVerifyResult = undefined;
228
+ this.hierarchicalPlan = undefined;
229
+ this.planGraph = null;
230
+ this.parallelStepResults = new Map();
231
+ this.parallelExecutionDone = false;
232
+ this.emit("engine:start", goal);
233
+ this._logger.logInput(goal);
234
+ this.reflection.think("start", `Goal received: "${goal}"`);
235
+ try {
236
+ // 0. Intent inference pre-processing (refine ambiguous goals)
237
+ if (this.config.enableIntentInference) {
238
+ try {
239
+ const intentEngine = new IntentInferenceEngine({
240
+ byokConfig: this.config.byokConfig,
241
+ projectPath: this.config.projectPath,
242
+ maxContextTokens: 4000,
243
+ });
244
+ const intentResult = await intentEngine.infer(goal);
245
+ this.emit("intent:inferred", intentResult);
246
+ if (intentResult.isAmbiguous) {
247
+ this._logger.info("system", `Intent inference: ambiguous input refined — "${goal}" → "${intentResult.refinedGoal}" (category: ${intentResult.category}, confidence: ${intentResult.confidence.toFixed(2)})`);
248
+ this.reflection.think("analyze", `Intent inference refined ambiguous goal: "${goal}" → "${intentResult.refinedGoal}" [${intentResult.category}, confidence=${intentResult.confidence.toFixed(2)}]`);
249
+ goal = intentResult.refinedGoal;
250
+ }
251
+ else {
252
+ this._logger.info("system", `Intent inference: input is clear (category: ${intentResult.category}, confidence: ${intentResult.confidence.toFixed(2)})`);
253
+ }
254
+ }
255
+ catch (intentErr) {
256
+ this._logger.warn("system", `Intent inference failed (continuing with original goal): ${intentErr instanceof Error ? intentErr.message : String(intentErr)}`);
257
+ }
258
+ }
259
+ // 1. Build codebase index (if enabled and not already built)
260
+ if (this.config.enableCodebaseIndex && !this.codebaseContext) {
261
+ const languageSupport = new LanguageSupport();
262
+ this.codebaseContext = new CodebaseContext(this.config.projectPath, languageSupport);
263
+ this._logger.info("system", "Building codebase index", { projectPath: this.config.projectPath });
264
+ this.reflection.think("analyze", "Building codebase index...");
265
+ await this.codebaseContext.buildIndex();
266
+ const stats = this.codebaseContext.getStats();
267
+ this._logger.info("system", `Codebase indexed: ${stats.totalFiles} files, ${stats.totalSymbols} symbols`);
268
+ this.reflection.think("analyze", `Index built: ${stats.totalFiles} files, ${stats.totalSymbols} symbols`);
269
+ }
270
+ // 1b. Conditional VectorIndex initialization (requires pgvector)
271
+ if (this.config.enableVectorSearch && !this.vectorIndex) {
272
+ // VectorIndex requires a real SQLExecutor and EmbeddingProvider.
273
+ // When no DB is available, create a no-op stub so the engine
274
+ // doesn't crash — vector search simply returns empty results.
275
+ const noopSqlExecutor = {
276
+ query: async () => ({ rows: [] }),
277
+ };
278
+ const noopEmbeddingProvider = {
279
+ embed: async (texts) => texts.map(() => []),
280
+ dimension: 1536,
281
+ };
282
+ this.vectorIndex = new VectorIndex({
283
+ projectId: this.config.projectPath,
284
+ sqlExecutor: noopSqlExecutor,
285
+ embeddingProvider: noopEmbeddingProvider,
286
+ });
287
+ this._logger.info("system", "VectorIndex initialized (no-op stub — supply real DB for pgvector search)");
288
+ }
289
+ // 1c. MCP Client initialization (optional — connect to external MCP servers)
290
+ if (this.mcpServerConfigs.length > 0) {
291
+ try {
292
+ this.mcpClient = new MCPClient({
293
+ servers: this.mcpServerConfigs.map((s) => ({
294
+ name: s.name,
295
+ transport: "stdio",
296
+ command: s.command,
297
+ args: s.args ?? [],
298
+ })),
299
+ });
300
+ await this.mcpClient.connectAll();
301
+ this.mcpToolDefinitions = this.mcpClient.toToolDefinitions();
302
+ this._logger.info("system", `MCP: discovered ${this.mcpToolDefinitions.length} tool(s) from ${this.mcpServerConfigs.length} server(s)`);
303
+ this.reflection.think("analyze", `MCP tools available: ${this.mcpToolDefinitions.length}`);
304
+ }
305
+ catch (mcpErr) {
306
+ this._logger.warn("system", `MCP connection failed (continuing without MCP tools): ${mcpErr instanceof Error ? mcpErr.message : String(mcpErr)}`);
307
+ this.mcpClient = null;
308
+ this.mcpToolDefinitions = [];
309
+ }
310
+ }
311
+ // 2. Create state machine with wired callbacks
312
+ const context = this.buildStateMachineContext();
313
+ this.stateMachine = new AgentStateMachine(context, {
314
+ maxFixAttempts: this.config.maxFixAttempts,
315
+ skipDesignForSimple: this.config.skipDesignForSimple,
316
+ enableParallel: this.config.enableParallel,
317
+ });
318
+ // Wire state machine events
319
+ this.stateMachine.on("phase:enter", (phase) => {
320
+ this.emit("phase:enter", phase);
321
+ if (this.perfOptimizer) {
322
+ this.perfOptimizer.startPhase(phase);
323
+ }
324
+ // Checkpoint after each phase transition
325
+ this.checkpoint().catch((err) => {
326
+ console.warn("[YUAN] Checkpoint failed:", err instanceof Error ? err.message : err);
327
+ });
328
+ });
329
+ this.stateMachine.on("phase:exit", (phase) => {
330
+ this.emit("phase:exit", phase);
331
+ if (this.perfOptimizer) {
332
+ this.perfOptimizer.endPhase(phase);
333
+ }
334
+ });
335
+ this.stateMachine.on("verify:result", (r) => {
336
+ // Convert VerifyResult to a lightweight emit; deepVerify result is emitted separately
337
+ this.emit("verify:result", this.lastVerifyResult);
338
+ });
339
+ // 3. Run state machine
340
+ const finalState = await this.stateMachine.run(goal, this.abortController.signal);
341
+ // 4. Build result
342
+ const result = this.buildResult(finalState, startTime);
343
+ // Performance report (if tracking enabled)
344
+ if (this.perfOptimizer) {
345
+ const perfReport = this.perfOptimizer.generateReport(this.sessionId);
346
+ this._logger.info("system", `Perf: efficiency score ${perfReport.efficiencyScore}/100`);
347
+ if (perfReport.bottlenecks.length > 0) {
348
+ this._logger.info("system", `Perf: ${perfReport.bottlenecks.length} bottleneck(s) detected`);
349
+ }
350
+ }
351
+ // Disconnect MCP servers (cleanup)
352
+ if (this.mcpClient) {
353
+ await this.mcpClient.disconnectAll().catch(() => {
354
+ // Best-effort cleanup
355
+ });
356
+ }
357
+ this._logger.logOutput(result.summary, result.success, result.totalTokens);
358
+ await this._logger.flush();
359
+ this.emit("engine:complete", result);
360
+ return result;
361
+ }
362
+ catch (err) {
363
+ const error = err instanceof Error ? err : new Error(String(err));
364
+ this._logger.error("error", `Execution failed: ${error.message}`);
365
+ await this._logger.flush();
366
+ this.emit("engine:error", error);
367
+ return {
368
+ success: false,
369
+ termination: { reason: "ERROR", error: error.message },
370
+ finalPhase: this.stateMachine?.getState().phase ?? "idle",
371
+ changedFiles: [...this.changedFiles],
372
+ summary: `Execution failed: ${error.message}`,
373
+ totalTokens: { input: 0, output: 0 },
374
+ totalIterations: 0,
375
+ totalToolCalls: 0,
376
+ durationMs: Date.now() - startTime,
377
+ monologue: [...this.reflection.getMonologue()],
378
+ learnings: [...this.reflection.getAllLearnings()],
379
+ };
380
+ }
381
+ }
382
+ /**
383
+ * 런타임 상태를 디스크에 체크포인트로 저장한다.
384
+ * sessionPersistence가 없으면 아무것도 하지 않는다.
385
+ * 실패해도 실행을 중단하지 않는다 (경고 로그만 남김).
386
+ *
387
+ * @param stepIndex - 현재 step 인덱스 (있으면)
388
+ */
389
+ async checkpoint(stepIndex) {
390
+ if (!this.sessionPersistence)
391
+ return;
392
+ try {
393
+ await this.sessionPersistence.saveRuntimeState(this.sessionId, {
394
+ planGraphState: this.planGraph?.toJSON() ?? null,
395
+ stateMachinePhase: this.stateMachine?.getState().phase,
396
+ stepIndex,
397
+ reflectionLearnings: [...this.reflection.getAllLearnings()],
398
+ reflectionMonologue: [...this.reflection.getMonologue()],
399
+ });
400
+ }
401
+ catch (err) {
402
+ this._logger.warn("system", `Checkpoint save failed: ${err instanceof Error ? err.message : String(err)}`);
403
+ }
404
+ }
405
+ /**
406
+ * 실행을 중단한다.
407
+ * 현재 진행 중인 AgentLoop와 StateMachine이 다음 체크포인트에서 중단된다.
408
+ */
409
+ abort() {
410
+ // Emergency checkpoint save before stopping
411
+ this.checkpoint().catch(() => {
412
+ // Ignore — best-effort save during abort
413
+ });
414
+ this.abortController.abort();
415
+ }
416
+ // ─── StateMachineContext Builder ────────────────────────────────
417
+ /**
418
+ * StateMachine에 주입할 콜백 컨텍스트를 생성한다.
419
+ * 각 콜백이 적절한 하위 시스템(AgentLoop, HierarchicalPlanner 등)을 호출한다.
420
+ */
421
+ buildStateMachineContext() {
422
+ return {
423
+ analyzeFn: this.analyzeGoal.bind(this),
424
+ designFn: this.designApproaches.bind(this),
425
+ planFn: this.createPlan.bind(this),
426
+ executeFn: this.executeStepWithParallel.bind(this),
427
+ verifyFn: this.verifyWork.bind(this),
428
+ fixFn: this.fixErrors.bind(this),
429
+ replanFn: this.replanWork.bind(this),
430
+ delegateFn: this.delegateToUser.bind(this),
431
+ };
432
+ }
433
+ // ─── Phase Callbacks ───────────────────────────────────────────
434
+ /**
435
+ * analyze phase 콜백 — CodebaseContext + LLM으로 목표를 분석한다.
436
+ *
437
+ * @param goal - 사용자 목표
438
+ * @param state - 현재 에이전트 상태
439
+ * @returns 복잡도 판정과 분석 컨텍스트
440
+ */
441
+ async analyzeGoal(goal, state) {
442
+ const exitAnalyze = this._logger.enterLayer("analyze", `Analyzing goal: "${goal}"`);
443
+ this.reflection.think("analyze", `Analyzing goal: "${goal}"`);
444
+ let codebaseStats = {};
445
+ if (this.codebaseContext) {
446
+ codebaseStats = this.codebaseContext.getStats();
447
+ // Semantic search for relevant symbols
448
+ const relevant = this.codebaseContext.searchSymbols(goal, 10);
449
+ if (relevant.length > 0) {
450
+ const symbolNames = relevant.map((r) => `${r.symbol.name} (${r.symbol.kind}) in ${r.symbol.file}`);
451
+ this.reflection.think("analyze", `Found ${relevant.length} relevant symbols: ${symbolNames.slice(0, 5).join(", ")}`);
452
+ }
453
+ }
454
+ const analysisPrompt = this.buildAnalysisPrompt(goal, codebaseStats);
455
+ const response = await this.llmClient.chat([
456
+ { role: "system", content: analysisPrompt },
457
+ { role: "user", content: goal },
458
+ ]);
459
+ const content = response.content ?? "";
460
+ // Parse complexity from LLM response
461
+ let complexity = "moderate";
462
+ const complexityMatch = content.match(/\b(trivial|simple|moderate|complex|massive)\b/i);
463
+ if (complexityMatch) {
464
+ complexity = complexityMatch[1].toLowerCase();
465
+ }
466
+ this._logger.logDecision("Goal complexity assessment", ["trivial", "simple", "moderate", "complex", "massive"], complexity, `LLM analysis determined complexity based on goal and codebase context`);
467
+ this.reflection.think("analyze", `Analysis complete — complexity: ${complexity}`);
468
+ exitAnalyze();
469
+ return { complexity, context: content };
470
+ }
471
+ /**
472
+ * design phase 콜백 — LLM에게 구현 접근법 제안을 요청한다.
473
+ *
474
+ * @param goal - 사용자 목표
475
+ * @param context - analyze phase에서 수집한 컨텍스트
476
+ * @returns 제안된 접근법 목록
477
+ */
478
+ async designApproaches(goal, context) {
479
+ const exitDesign = this._logger.enterLayer("design", "Generating approach options");
480
+ this.reflection.think("design", "Generating approach options...");
481
+ const prompt = `You are a senior architect. Based on the analysis, propose 2-3 implementation approaches.
482
+
483
+ ## Analysis Context
484
+ ${context}
485
+
486
+ ## Goal
487
+ ${goal}
488
+
489
+ ## Output Format
490
+ Respond with ONLY a JSON array (no markdown fences):
491
+ [
492
+ {
493
+ "id": 1,
494
+ "name": "approach name",
495
+ "description": "what this approach does",
496
+ "pros": ["advantage 1"],
497
+ "cons": ["disadvantage 1"],
498
+ "estimatedComplexity": "low",
499
+ "recommended": true
500
+ }
501
+ ]
502
+
503
+ Complexity: "low" | "medium" | "high"
504
+ Exactly one approach should have recommended=true.`;
505
+ const response = await this.llmClient.chat([
506
+ { role: "system", content: prompt },
507
+ { role: "user", content: goal },
508
+ ]);
509
+ const content = response.content ?? "[]";
510
+ try {
511
+ const jsonStr = this.extractJson(content);
512
+ const parsed = JSON.parse(jsonStr);
513
+ if (Array.isArray(parsed) && parsed.length > 0) {
514
+ const recommended = parsed.find((a) => a.recommended);
515
+ this._logger.logDecision("Select implementation approach", parsed.map((a) => a.name), recommended?.name ?? parsed[0].name, `LLM recommended approach based on goal analysis`);
516
+ this.reflection.think("design", `Generated ${parsed.length} approaches. Recommended: ${recommended?.name ?? "none"}`);
517
+ exitDesign();
518
+ return parsed;
519
+ }
520
+ }
521
+ catch {
522
+ // Parse failure — fall through
523
+ }
524
+ // Default: single direct approach
525
+ const defaultApproach = {
526
+ id: 1,
527
+ name: "direct",
528
+ description: "Direct implementation based on analysis",
529
+ pros: ["Simple", "Fast"],
530
+ cons: ["May miss edge cases"],
531
+ estimatedComplexity: "medium",
532
+ recommended: true,
533
+ };
534
+ this._logger.logReasoning("LLM response could not be parsed, falling back to default direct approach");
535
+ this.reflection.think("design", "Using default direct approach");
536
+ exitDesign();
537
+ return [defaultApproach];
538
+ }
539
+ /**
540
+ * plan phase 콜백 — HierarchicalPlanner로 3-level 실행 계획을 수립한다.
541
+ *
542
+ * @param goal - 사용자 목표
543
+ * @param approach - 선택된 접근법
544
+ * @returns 실행 계획 (ExecutionPlan 형식)
545
+ */
546
+ async createPlan(goal, approach) {
547
+ const exitPlan = this._logger.enterLayer("plan", `Creating plan with approach: "${approach.name}"`);
548
+ this.reflection.think("plan", `Creating plan with approach: "${approach.name}"`);
549
+ if (this.config.enableHierarchicalPlanning) {
550
+ const planner = new HierarchicalPlanner({
551
+ projectPath: this.config.projectPath,
552
+ });
553
+ const hPlan = await planner.createHierarchicalPlan(goal, this.llmClient);
554
+ this.hierarchicalPlan = hPlan;
555
+ this._logger.info("system", `Hierarchical plan: ${hPlan.tactical.length} tasks, ${hPlan.totalEstimatedIterations} iterations, ${hPlan.parallelizableGroups.length} parallel groups`);
556
+ this.reflection.think("plan", `Hierarchical plan created: ${hPlan.tactical.length} tactical tasks, ` +
557
+ `${hPlan.totalEstimatedIterations} estimated iterations, ` +
558
+ `${hPlan.parallelizableGroups.length} parallel groups`);
559
+ const executionPlan = planner.toExecutionPlan(hPlan);
560
+ // Initialize PlanGraphManager for step progress tracking
561
+ this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, executionPlan);
562
+ this._logger.info("system", `PlanGraph initialized: ${executionPlan.steps.length} nodes, ${this.planGraph.getState().parallelGroups.length} parallel groups`);
563
+ this.reflection.think("plan", `PlanGraph initialized with ${executionPlan.steps.length} nodes`);
564
+ exitPlan();
565
+ return executionPlan;
566
+ }
567
+ // Fallback: single-step plan
568
+ this._logger.logReasoning("Hierarchical planning disabled, using single-step fallback");
569
+ this.reflection.think("plan", "Using single-step fallback plan");
570
+ const fallbackPlan = {
571
+ goal,
572
+ steps: [
573
+ {
574
+ id: "step-1",
575
+ goal: `${approach.description}: ${goal}`,
576
+ targetFiles: [],
577
+ readFiles: [],
578
+ tools: ["file_read", "file_write", "file_edit", "grep", "glob"],
579
+ estimatedIterations: 10,
580
+ dependsOn: [],
581
+ },
582
+ ],
583
+ estimatedTokens: 50_000,
584
+ };
585
+ // Initialize PlanGraphManager for fallback plan too
586
+ this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, fallbackPlan);
587
+ this._logger.info("system", `PlanGraph initialized (fallback): 1 node`);
588
+ exitPlan();
589
+ return fallbackPlan;
590
+ }
591
+ /**
592
+ * 병렬 실행 가능 여부에 따라 DAG 기반 병렬 실행 또는 단일 step 실행을 선택한다.
593
+ *
594
+ * StateMachine의 `handleParallel` phase에서 호출될 때:
595
+ * - 첫 호출 시 DAG 기반 병렬 실행을 수행하고 모든 결과를 캐시한다.
596
+ * - 이후 호출은 캐시에서 결과를 반환한다.
597
+ *
598
+ * StateMachine의 `handleImplement` phase에서 호출될 때:
599
+ * - 기존 순차 실행을 그대로 사용한다.
600
+ *
601
+ * @param plan - 실행 계획
602
+ * @param stepIndex - 실행할 step 인덱스
603
+ * @param state - 현재 에이전트 상태
604
+ * @returns step 실행 결과
605
+ */
606
+ async executeStepWithParallel(plan, stepIndex, state) {
607
+ // Parallel DAG execution: if enabled and PlanGraph exists and plan has multiple steps
608
+ const shouldRunParallel = this.config.enableParallelExecution &&
609
+ this.planGraph !== null &&
610
+ plan.steps.length > 1;
611
+ if (!shouldRunParallel) {
612
+ // Sequential fallback
613
+ return this.executeStep(plan, stepIndex, state);
614
+ }
615
+ // If DAG execution already ran, return cached result
616
+ if (this.parallelExecutionDone) {
617
+ const cached = this.parallelStepResults.get(stepIndex);
618
+ if (cached)
619
+ return cached;
620
+ // No cached result means this step was not reached by DAG (failed deps, etc.)
621
+ return {
622
+ stepIndex,
623
+ phase: "implement",
624
+ success: false,
625
+ output: `Step ${stepIndex} was not executed (blocked by dependency failure or skipped)`,
626
+ changedFiles: [],
627
+ tokensUsed: 0,
628
+ durationMs: 0,
629
+ };
630
+ }
631
+ // First call triggers full DAG execution for ALL steps
632
+ await this.executeStepsWithDAG(plan, state);
633
+ this.parallelExecutionDone = true;
634
+ // Return this step's result from cache
635
+ const result = this.parallelStepResults.get(stepIndex);
636
+ if (result)
637
+ return result;
638
+ return {
639
+ stepIndex,
640
+ phase: "implement",
641
+ success: false,
642
+ output: `Step ${stepIndex} was not executed during DAG execution`,
643
+ changedFiles: [],
644
+ tokensUsed: 0,
645
+ durationMs: 0,
646
+ };
647
+ }
648
+ /**
649
+ * PlanGraph DAG 기반 병렬 step 실행.
650
+ *
651
+ * PlanGraphManager의 의존성 그래프를 따라:
652
+ * 1. 의존성이 모두 완료된 ready 노드를 가져온다.
653
+ * 2. maxParallelAgents만큼 동시에 실행한다.
654
+ * 3. 완료되면 의존 노드를 ready로 전환하고 반복한다.
655
+ * 4. 모든 노드가 종료 상태가 될 때까지 반복한다.
656
+ *
657
+ * @param plan - 실행 계획
658
+ * @param state - 현재 에이전트 상태
659
+ */
660
+ async executeStepsWithDAG(plan, state) {
661
+ if (!this.planGraph)
662
+ return;
663
+ const maxConcurrent = this.config.maxParallelAgents;
664
+ this._logger.info("system", `DAG parallel execution started: ${plan.steps.length} steps, max ${maxConcurrent} concurrent`);
665
+ this.reflection.think("implement", `Starting DAG-based parallel execution: ${plan.steps.length} steps, max ${maxConcurrent} concurrent`);
666
+ while (true) {
667
+ // Check abort
668
+ if (this.abortController.signal.aborted) {
669
+ this._logger.info("system", "DAG parallel execution aborted");
670
+ break;
671
+ }
672
+ // Get nodes that are ready (all dependencies completed)
673
+ const readyNodes = this.planGraph.getReadyNodes();
674
+ if (readyNodes.length === 0) {
675
+ // Check if all done or stuck
676
+ if (this.planGraph.isComplete()) {
677
+ this._logger.info("system", "DAG parallel execution: all nodes complete");
678
+ break;
679
+ }
680
+ // If there are running nodes, wait for them to finish
681
+ const graphState = this.planGraph.getState();
682
+ if (graphState.runningNodes.length > 0) {
683
+ await new Promise(resolve => setTimeout(resolve, 100));
684
+ continue;
685
+ }
686
+ // No ready, no running = deadlock (all remaining nodes are blocked/failed)
687
+ this._logger.warn("system", "DAG parallel execution: deadlock — no ready or running nodes, but graph not complete");
688
+ break;
689
+ }
690
+ // Take up to maxConcurrent ready nodes
691
+ const batch = readyNodes.slice(0, maxConcurrent);
692
+ this._logger.info("system", `DAG batch: executing ${batch.length} steps in parallel [${batch.map(n => n.id).join(", ")}]`);
693
+ this.reflection.think("implement", `Parallel batch: ${batch.map(n => `"${n.goal}" (${n.id})`).join(", ")}`);
694
+ // Execute batch in parallel using Promise.allSettled
695
+ await Promise.allSettled(batch.map(async (node) => {
696
+ const stepIndex = plan.steps.findIndex(s => s.id === node.id);
697
+ if (stepIndex === -1) {
698
+ this._logger.warn("system", `DAG: node "${node.id}" not found in plan steps`);
699
+ return;
700
+ }
701
+ try {
702
+ // executeStep already handles markRunning/markCompleted/markFailed
703
+ const stepResult = await this.executeStep(plan, stepIndex, state);
704
+ // Cache the result
705
+ this.parallelStepResults.set(stepIndex, stepResult);
706
+ // Update state tracking
707
+ state.currentStepIndex = Math.max(state.currentStepIndex, stepIndex + 1);
708
+ }
709
+ catch (err) {
710
+ // executeStep should not throw (it handles errors internally),
711
+ // but just in case, cache a failure result
712
+ const errorMessage = err instanceof Error ? err.message : String(err);
713
+ this._logger.error("error", `DAG: unexpected error executing step ${stepIndex}: ${errorMessage}`);
714
+ this.parallelStepResults.set(stepIndex, {
715
+ stepIndex,
716
+ phase: "implement",
717
+ success: false,
718
+ output: `Unexpected error: ${errorMessage}`,
719
+ changedFiles: [],
720
+ tokensUsed: 0,
721
+ durationMs: 0,
722
+ });
723
+ }
724
+ }));
725
+ // Checkpoint after each parallel batch
726
+ await this.checkpoint(state.currentStepIndex);
727
+ // Log progress
728
+ const progress = this.planGraph.getProgress();
729
+ this._logger.info("system", `DAG progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
730
+ }
731
+ // Final summary
732
+ const progress = this.planGraph.getProgress();
733
+ const graphState = this.planGraph.getState();
734
+ this._logger.info("system", `DAG parallel execution complete: ${progress.completed}/${progress.total} succeeded, ${graphState.failedNodes.length} failed, ${graphState.skippedNodes.length} skipped`);
735
+ this.reflection.think("implement", `DAG execution complete: ${progress.completed}/${progress.total} steps, ${graphState.failedNodes.length} failed`);
736
+ }
737
+ /**
738
+ * implement phase 콜백 — AgentLoop로 개별 step을 실행한다.
739
+ *
740
+ * @param plan - 실행 계획
741
+ * @param stepIndex - 실행할 step 인덱스
742
+ * @param state - 현재 에이전트 상태
743
+ * @returns step 실행 결과
744
+ */
745
+ async executeStep(plan, stepIndex, state) {
746
+ const step = plan.steps[stepIndex];
747
+ if (!step) {
748
+ this._logger.warn("error", `Step ${stepIndex} not found in plan`);
749
+ return {
750
+ stepIndex,
751
+ phase: "implement",
752
+ success: false,
753
+ output: `Step ${stepIndex} not found in plan`,
754
+ changedFiles: [],
755
+ tokensUsed: 0,
756
+ durationMs: 0,
757
+ };
758
+ }
759
+ const exitImplement = this._logger.enterLayer("implement", `Step ${stepIndex + 1}/${plan.steps.length}: "${step.goal}"`);
760
+ this.reflection.think("implement", `Executing step ${stepIndex + 1}/${plan.steps.length}: "${step.goal}"`);
761
+ // Mark step as running in PlanGraph
762
+ const stepId = step.id;
763
+ if (this.planGraph) {
764
+ try {
765
+ this.planGraph.markRunning(stepId);
766
+ this._logger.info("system", `PlanGraph: node "${stepId}" → running`);
767
+ }
768
+ catch (pgErr) {
769
+ // Node may not be in ready state (e.g., if called out of order)
770
+ this._logger.warn("system", `PlanGraph: failed to mark "${stepId}" running: ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
771
+ }
772
+ }
773
+ // Snapshot target files before modification
774
+ await this.snapshotFiles(step.targetFiles);
775
+ // ─── Speculative Execution (opt-in) ─────────────────────────
776
+ if (this.config.enableSpeculative) {
777
+ try {
778
+ this._logger.info("system", `Speculative execution enabled for step ${stepIndex + 1}`);
779
+ this.reflection.think("implement", `Using speculative execution for step ${stepIndex + 1}`);
780
+ const specExecutor = new SpeculativeExecutor({
781
+ maxApproaches: this.config.speculativeMaxApproaches,
782
+ approachTimeout: 120_000,
783
+ minQualityThreshold: 60,
784
+ byokConfig: this.config.byokConfig,
785
+ projectPath: this.config.projectPath,
786
+ });
787
+ // Forward speculative events
788
+ const specEvents = [
789
+ "speculative:start",
790
+ "speculative:approach:start",
791
+ "speculative:approach:complete",
792
+ "speculative:evaluation",
793
+ "speculative:complete",
794
+ ];
795
+ for (const eventName of specEvents) {
796
+ specExecutor.on(eventName, (...args) => {
797
+ this.emit(eventName, ...args);
798
+ });
799
+ }
800
+ // Build codebase context summary for speculative executor
801
+ let codebaseContextSummary = "";
802
+ if (this.codebaseContext) {
803
+ const stats = this.codebaseContext.getStats();
804
+ codebaseContextSummary = `Project: ${stats.totalFiles} files, ${stats.totalSymbols} symbols. Path: ${this.config.projectPath}`;
805
+ }
806
+ const specResult = await specExecutor.execute(step.goal, this.config.toolExecutor, codebaseContextSummary);
807
+ if (specResult.winner) {
808
+ this._logger.info("system", `Speculative execution winner: "${specResult.winner.approach.strategy}" (quality: ${specResult.winner.qualityScore}, tokens: ${specResult.winner.tokensUsed})`);
809
+ this.reflection.think("implement", `Speculative winner: ${specResult.winner.approach.strategy} — ${specResult.selectionReason}`);
810
+ // Apply winner's file changes
811
+ for (const [filePath] of specResult.winner.changes) {
812
+ this.changedFiles.add(filePath);
813
+ }
814
+ // Update PlanGraph
815
+ if (this.planGraph) {
816
+ try {
817
+ this.planGraph.markCompleted(stepId, `Speculative execution (${specResult.winner.approach.strategy}): quality ${specResult.winner.qualityScore}`, [...specResult.winner.changes.keys()], { input: specResult.totalTokensUsed, output: 0 });
818
+ this._logger.info("system", `PlanGraph: node "${stepId}" → completed (speculative)`);
819
+ }
820
+ catch (pgErr) {
821
+ this._logger.warn("system", `PlanGraph: failed to update "${stepId}": ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
822
+ }
823
+ }
824
+ // Checkpoint and return
825
+ await this.checkpoint(stepIndex);
826
+ exitImplement();
827
+ return {
828
+ stepIndex,
829
+ phase: "implement",
830
+ success: specResult.winner.success,
831
+ output: `Speculative execution (${specResult.winner.approach.strategy}): ${specResult.selectionReason}`,
832
+ changedFiles: [...specResult.winner.changes.keys()],
833
+ tokensUsed: specResult.totalTokensUsed,
834
+ durationMs: specResult.winner.durationMs,
835
+ };
836
+ }
837
+ // No winner — fall through to normal AgentLoop execution
838
+ this._logger.info("system", "Speculative execution: no winner found, falling back to normal execution");
839
+ this.reflection.think("implement", "Speculative execution produced no winner — falling back to AgentLoop");
840
+ if (specResult.learnings.length > 0) {
841
+ this.reflection.think("implement", `Speculative learnings: ${specResult.learnings.join("; ")}`);
842
+ }
843
+ }
844
+ catch (specErr) {
845
+ this._logger.warn("system", `Speculative execution failed (falling back to normal): ${specErr instanceof Error ? specErr.message : String(specErr)}`);
846
+ }
847
+ }
848
+ // ─── Normal AgentLoop Execution ─────────────────────────────
849
+ // Build step-specific system prompt
850
+ const systemPrompt = this.buildStepPrompt(plan, step, stepIndex);
851
+ // Create and run AgentLoop for this step
852
+ const loop = this.createAgentLoop(systemPrompt);
853
+ this.wireAgentLoopEvents(loop);
854
+ const startMs = Date.now();
855
+ const termination = await loop.run(step.goal);
856
+ const durationMs = Date.now() - startMs;
857
+ // Track last termination
858
+ this.lastTermination = termination;
859
+ // Collect changed files from loop
860
+ const loopUsage = loop.getTokenUsage();
861
+ const stepChangedFiles = [];
862
+ // Extract changed files from agent events (tracked via tool results)
863
+ // The changedFiles set is updated via wireAgentLoopEvents
864
+ for (const file of this.changedFiles) {
865
+ if (step.targetFiles.includes(file) || step.targetFiles.length === 0) {
866
+ stepChangedFiles.push(file);
867
+ }
868
+ }
869
+ const success = termination.reason === "GOAL_ACHIEVED";
870
+ // Update PlanGraph with step outcome
871
+ if (this.planGraph) {
872
+ try {
873
+ if (success) {
874
+ this.planGraph.markCompleted(stepId, termination.reason === "GOAL_ACHIEVED"
875
+ ? termination.summary
876
+ : "completed", stepChangedFiles, { input: loopUsage.input, output: loopUsage.output });
877
+ this._logger.info("system", `PlanGraph: node "${stepId}" → completed`);
878
+ }
879
+ else {
880
+ const errorMsg = termination.reason === "ERROR"
881
+ ? termination.error
882
+ : `Step failed: ${termination.reason}`;
883
+ this.planGraph.markFailed(stepId, errorMsg);
884
+ this._logger.info("system", `PlanGraph: node "${stepId}" → failed`);
885
+ }
886
+ }
887
+ catch (pgErr) {
888
+ this._logger.warn("system", `PlanGraph: failed to update "${stepId}": ${pgErr instanceof Error ? pgErr.message : String(pgErr)}`);
889
+ }
890
+ // Log plan progress after each step
891
+ const progress = this.planGraph.getProgress();
892
+ this._logger.info("system", `PlanGraph progress: ${progress.completed}/${progress.total} (${progress.percent}%)`);
893
+ }
894
+ this._logger.logDecision(`Step ${stepIndex + 1} outcome`, ["succeeded", "failed"], success ? "succeeded" : "failed", `AgentLoop terminated with reason: ${termination.reason}`);
895
+ this.reflection.think("implement", `Step ${stepIndex + 1} ${success ? "succeeded" : "failed"}: ${termination.reason === "GOAL_ACHIEVED"
896
+ ? termination.summary
897
+ : termination.reason}`);
898
+ // Checkpoint after step completes (success or failure)
899
+ await this.checkpoint(stepIndex);
900
+ exitImplement();
901
+ return {
902
+ stepIndex,
903
+ phase: "implement",
904
+ success,
905
+ output: termination.reason === "GOAL_ACHIEVED"
906
+ ? termination.summary
907
+ : termination.reason === "ERROR"
908
+ ? termination.error
909
+ : `Terminated: ${termination.reason}`,
910
+ changedFiles: stepChangedFiles,
911
+ tokensUsed: loopUsage.input + loopUsage.output,
912
+ durationMs,
913
+ };
914
+ }
915
+ /**
916
+ * verify phase 콜백 — SelfReflection.deepVerify로 6-dimension 검증을 수행한다.
917
+ *
918
+ * @param state - 현재 에이전트 상태
919
+ * @returns 검증 결과 (StateMachine VerifyResult 형식)
920
+ */
921
+ async verifyWork(state) {
922
+ const exitVerify = this._logger.enterLayer("verify", "Starting deep verification");
923
+ this.reflection.think("verify", "Starting deep verification...");
924
+ if (!this.config.enableDeepVerify || this.changedFiles.size === 0) {
925
+ this._logger.logReasoning("Deep verify skipped", ["run deep verify", "skip deep verify"], "skip deep verify", undefined, "Deep verify disabled or no changed files");
926
+ this.reflection.think("verify", "Deep verify skipped (disabled or no changed files)");
927
+ exitVerify();
928
+ return {
929
+ verdict: "pass",
930
+ checks: {
931
+ buildSuccess: true,
932
+ typesSafe: true,
933
+ testsPass: true,
934
+ noRegressions: true,
935
+ followsPatterns: true,
936
+ securityClean: true,
937
+ },
938
+ issues: [],
939
+ suggestions: [],
940
+ confidence: 1.0,
941
+ };
942
+ }
943
+ // Collect current file contents
944
+ const changedFileContents = new Map();
945
+ for (const filePath of this.changedFiles) {
946
+ try {
947
+ const content = await readFile(filePath, "utf-8");
948
+ changedFileContents.set(filePath, content);
949
+ }
950
+ catch {
951
+ // File may have been deleted
952
+ changedFileContents.set(filePath, "");
953
+ }
954
+ }
955
+ // Gather project patterns from codebase context
956
+ const patterns = [];
957
+ if (this.codebaseContext) {
958
+ const stats = this.codebaseContext.getStats();
959
+ if (stats.totalFiles > 0) {
960
+ patterns.push(`Project has ${stats.totalFiles} files`);
961
+ }
962
+ }
963
+ // Run deep verification
964
+ const deepResult = await this.reflection.deepVerify(state.goal, changedFileContents, this.originalFiles, patterns, async (prompt) => {
965
+ const response = await this.llmClient.chat([
966
+ { role: "user", content: prompt },
967
+ ]);
968
+ return response.content ?? "";
969
+ });
970
+ this.lastVerifyResult = deepResult;
971
+ // Cross-check with PlanGraph completion status
972
+ if (this.planGraph) {
973
+ const pgComplete = this.planGraph.isComplete();
974
+ const progress = this.planGraph.getProgress();
975
+ this._logger.info("system", `PlanGraph completion check: complete=${pgComplete}, ${progress.completed}/${progress.total} nodes`);
976
+ if (!pgComplete && deepResult.verdict === "pass") {
977
+ this.reflection.think("verify", `Warning: Deep verify passed but PlanGraph shows ${progress.total - progress.completed} incomplete nodes`);
978
+ }
979
+ }
980
+ this._logger.logDecision("Verification verdict", ["pass", "warn", "fail"], deepResult.verdict, `Score: ${deepResult.overallScore}, confidence: ${deepResult.confidence}`);
981
+ this.reflection.think("verify", `Deep verification: ${deepResult.verdict} (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence})`);
982
+ // ─── Debate Orchestrator (optional) ───────────────────────
983
+ if (this.config.enableDebate && this.changedFiles.size > 0) {
984
+ try {
985
+ this._logger.info("system", "Running multi-agent debate verification...");
986
+ this.reflection.think("verify", "Starting debate orchestrator for code review...");
987
+ const debateOrchestrator = DebateOrchestrator.create({
988
+ projectPath: this.config.projectPath,
989
+ byokConfig: this.config.byokConfig,
990
+ toolExecutor: this.config.toolExecutor,
991
+ });
992
+ // Forward debate events
993
+ const debateEvents = [
994
+ "debate:start",
995
+ "debate:round:start",
996
+ "debate:round:end",
997
+ "debate:coder",
998
+ "debate:reviewer",
999
+ "debate:revision",
1000
+ "debate:verifier",
1001
+ "debate:pass",
1002
+ "debate:fail",
1003
+ "debate:token_usage",
1004
+ "debate:abort",
1005
+ ];
1006
+ for (const eventName of debateEvents) {
1007
+ debateOrchestrator.on(eventName, (...args) => {
1008
+ this.emit(eventName, ...args);
1009
+ });
1010
+ }
1011
+ const changedFilesSummary = [...this.changedFiles]
1012
+ .map((f) => {
1013
+ const content = changedFileContents.get(f) ?? "";
1014
+ return `## ${f}\n${content.slice(0, 2000)}`;
1015
+ })
1016
+ .join("\n\n");
1017
+ const debateResult = await debateOrchestrator.debate(state.goal, changedFilesSummary);
1018
+ this._logger.info("system", `Debate result: score=${debateResult.finalScore}, success=${debateResult.success}, rounds=${debateResult.rounds.length}`);
1019
+ this.reflection.think("verify", `Debate: ${debateResult.success ? "PASSED" : "FAILED"} (score: ${debateResult.finalScore}, ${debateResult.rounds.length} round(s))`);
1020
+ // If debate fails and deep verify passed, downgrade to "concern"
1021
+ if (!debateResult.success && deepResult.verdict === "pass") {
1022
+ deepResult.verdict = "concern";
1023
+ const reviewerIssues = debateResult.rounds
1024
+ .flatMap((r) => r.issues)
1025
+ .filter((i) => i.severity === "critical" || i.severity === "major");
1026
+ deepResult.suggestedFixes.push(...reviewerIssues.map((i) => ({
1027
+ dimension: "correctness",
1028
+ severity: i.severity === "critical" ? "critical" : "high",
1029
+ description: `[debate] ${i.description}`,
1030
+ file: i.file,
1031
+ autoFixable: false,
1032
+ })));
1033
+ this.reflection.think("verify", `Debate downgraded verdict to "concern" due to ${reviewerIssues.length} critical/major issue(s)`);
1034
+ }
1035
+ }
1036
+ catch (debateErr) {
1037
+ this._logger.warn("system", `Debate orchestrator failed (continuing): ${debateErr instanceof Error ? debateErr.message : String(debateErr)}`);
1038
+ }
1039
+ }
1040
+ // ─── Security Scanner (optional) ──────────────────────────
1041
+ if (this.config.enableSecurityScan && this.changedFiles.size > 0) {
1042
+ try {
1043
+ this._logger.info("system", "Running security scan on changed files...");
1044
+ this.reflection.think("verify", "Running security scanner...");
1045
+ const scanner = new SecurityScanner({
1046
+ projectPath: this.config.projectPath,
1047
+ });
1048
+ const changedFileList = [...this.changedFiles];
1049
+ const scanResult = await scanner.scan(changedFileList);
1050
+ this._logger.info("system", `Security scan: passed=${scanResult.passed}, findings=${scanResult.summary.total} (critical=${scanResult.summary.critical}, high=${scanResult.summary.high})`);
1051
+ this.reflection.think("verify", `Security scan: ${scanResult.passed ? "PASSED" : "FAILED"} — ${scanResult.summary.total} finding(s) (${scanResult.summary.critical} critical, ${scanResult.summary.high} high)`);
1052
+ // If critical findings found, add them to suggestedFixes and downgrade verdict
1053
+ if (scanResult.summary.critical > 0 || scanResult.summary.high > 0) {
1054
+ const securityFindings = scanResult.findings
1055
+ .filter((f) => f.severity === "critical" || f.severity === "high");
1056
+ deepResult.suggestedFixes.push(...securityFindings.map((f) => ({
1057
+ dimension: "security",
1058
+ severity: f.severity === "critical" ? "critical" : "high",
1059
+ description: `[security] ${f.rule} in ${f.file}:${f.line} — ${f.message}`,
1060
+ file: f.file,
1061
+ autoFixable: false,
1062
+ })));
1063
+ if (scanResult.summary.critical > 0 && deepResult.verdict !== "fail") {
1064
+ deepResult.verdict = "fail";
1065
+ this.reflection.think("verify", `Security scanner escalated verdict to "fail" due to ${scanResult.summary.critical} critical finding(s)`);
1066
+ }
1067
+ else if (deepResult.verdict === "pass") {
1068
+ deepResult.verdict = "concern";
1069
+ this.reflection.think("verify", `Security scanner downgraded verdict to "concern" due to high-severity findings`);
1070
+ }
1071
+ }
1072
+ }
1073
+ catch (scanErr) {
1074
+ this._logger.warn("system", `Security scan failed (continuing): ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
1075
+ }
1076
+ }
1077
+ // ─── Doc Intelligence (optional) ──────────────────────────
1078
+ if (this.config.enableDocGeneration && this.changedFiles.size > 0) {
1079
+ try {
1080
+ this._logger.info("system", "Running documentation intelligence...");
1081
+ this.reflection.think("verify", "Analyzing documentation coverage...");
1082
+ const docIntel = new DocIntelligence({
1083
+ projectPath: this.config.projectPath,
1084
+ });
1085
+ const docCoverage = docIntel.analyzeCoverage(changedFileContents);
1086
+ this._logger.info("system", `Doc coverage: ${docCoverage.coveragePercent}% (grade: ${docCoverage.grade}), ${docCoverage.missing.length} undocumented symbol(s)`);
1087
+ this.reflection.think("verify", `Documentation: ${docCoverage.coveragePercent}% coverage (grade ${docCoverage.grade}), ${docCoverage.missing.length} missing`);
1088
+ // If coverage is below D grade, add as suggestedFix (non-blocking)
1089
+ if (docCoverage.grade === "D" || docCoverage.grade === "F") {
1090
+ const missingSymbols = docCoverage.missing
1091
+ .slice(0, 10)
1092
+ .map((m) => `${m.symbolName} (${m.symbolType}) in ${m.filePath}:${m.line}`);
1093
+ deepResult.suggestedFixes.push({
1094
+ dimension: "quality",
1095
+ severity: "low",
1096
+ description: `Documentation coverage is ${docCoverage.grade} (${docCoverage.coveragePercent}%). Missing JSDoc for: ${missingSymbols.join(", ")}`,
1097
+ autoFixable: false,
1098
+ });
1099
+ this.reflection.think("verify", `Low doc coverage (${docCoverage.grade}) — added suggestion for ${missingSymbols.length} symbol(s)`);
1100
+ }
1101
+ }
1102
+ catch (docErr) {
1103
+ this._logger.warn("system", `Doc intelligence failed (continuing): ${docErr instanceof Error ? docErr.message : String(docErr)}`);
1104
+ }
1105
+ }
1106
+ exitVerify();
1107
+ // Map DeepVerifyResult → VerifyResult
1108
+ return this.mapVerifyResult(deepResult);
1109
+ }
1110
+ /**
1111
+ * fix phase 콜백 — AgentLoop로 에러를 수정한다.
1112
+ *
1113
+ * @param errors - 수정할 에러 목록
1114
+ * @param state - 현재 에이전트 상태
1115
+ * @returns 수정 시도 결과
1116
+ */
1117
+ async fixErrors(errors, state) {
1118
+ const exitFix = this._logger.enterLayer("fix", `Fixing ${errors.length} error(s)`);
1119
+ const errorDescriptions = errors
1120
+ .map((e, i) => `${i + 1}. [${e.phase}] ${e.error}${e.suggestedFix ? ` (suggested: ${e.suggestedFix})` : ""}`)
1121
+ .join("\n");
1122
+ this._logger.info("error", `Errors to fix: ${errors.length}`, {
1123
+ errors: errors.map((e) => ({ phase: e.phase, error: e.error })),
1124
+ });
1125
+ this.reflection.think("fix", `Attempting to fix ${errors.length} error(s):\n${errorDescriptions}`);
1126
+ const fixPrompt = `You are fixing errors found during verification. Address each issue:
1127
+
1128
+ ## Errors to Fix
1129
+ ${errorDescriptions}
1130
+
1131
+ ## Changed Files
1132
+ ${[...this.changedFiles].join(", ")}
1133
+
1134
+ ## Instructions
1135
+ - Fix each error by reading the affected file and making corrections
1136
+ - Run relevant build/lint checks after each fix
1137
+ - Be precise — only change what's needed to fix the issue
1138
+ - If an error seems unfixable, explain why`;
1139
+ const loop = this.createAgentLoop(fixPrompt);
1140
+ this.wireAgentLoopEvents(loop);
1141
+ const startMs = Date.now();
1142
+ const termination = await loop.run(`Fix the following errors:\n${errorDescriptions}`);
1143
+ const durationMs = Date.now() - startMs;
1144
+ this.lastTermination = termination;
1145
+ const loopUsage = loop.getTokenUsage();
1146
+ const success = termination.reason === "GOAL_ACHIEVED";
1147
+ this._logger.logDecision("Fix attempt result", ["succeeded", "failed"], success ? "succeeded" : "failed", `Fix loop terminated with reason: ${termination.reason}`);
1148
+ this.reflection.think("fix", `Fix attempt ${success ? "succeeded" : "failed"}: ${termination.reason}`);
1149
+ exitFix();
1150
+ return {
1151
+ stepIndex: state.currentStepIndex,
1152
+ phase: "fix",
1153
+ success,
1154
+ output: termination.reason === "GOAL_ACHIEVED"
1155
+ ? termination.summary
1156
+ : `Fix attempt ended: ${termination.reason}`,
1157
+ changedFiles: [...this.changedFiles],
1158
+ tokensUsed: loopUsage.input + loopUsage.output,
1159
+ durationMs,
1160
+ };
1161
+ }
1162
+ /**
1163
+ * replan phase 콜백 — HierarchicalPlanner.replan으로 재계획을 수립한다.
1164
+ *
1165
+ * @param state - 현재 에이전트 상태
1166
+ * @returns 새로운 실행 계획
1167
+ */
1168
+ async replanWork(state) {
1169
+ const exitReplan = this._logger.enterLayer("replan", `Re-planning due to ${state.errors.length} error(s)`);
1170
+ this.reflection.think("replan", `Re-planning due to ${state.errors.length} error(s)`);
1171
+ if (this.config.enableHierarchicalPlanning && this.hierarchicalPlan) {
1172
+ const planner = new HierarchicalPlanner({
1173
+ projectPath: this.config.projectPath,
1174
+ });
1175
+ const errorDescriptions = state.errors
1176
+ .map((e) => e.error)
1177
+ .join("; ");
1178
+ const affectedTaskIds = state.errors
1179
+ .map((e) => state.plan?.steps[e.stepIndex]?.id)
1180
+ .filter((id) => !!id);
1181
+ const replanResult = await planner.replan(this.hierarchicalPlan, {
1182
+ type: "error",
1183
+ description: errorDescriptions,
1184
+ affectedTaskIds,
1185
+ severity: "major",
1186
+ }, this.llmClient);
1187
+ this._logger.logDecision("Replan strategy", ["retry", "simplify", "restructure", "abort"], replanResult.strategy, replanResult.reason);
1188
+ this.reflection.think("replan", `Re-plan strategy: ${replanResult.strategy} — ${replanResult.reason}`);
1189
+ // Convert modified tasks back to ExecutionPlan
1190
+ const newSteps = replanResult.modifiedTasks.map((task) => ({
1191
+ id: task.id,
1192
+ goal: task.description,
1193
+ targetFiles: task.targetFiles,
1194
+ readFiles: task.readFiles,
1195
+ tools: task.toolStrategy,
1196
+ estimatedIterations: task.estimatedIterations,
1197
+ dependsOn: task.dependsOn,
1198
+ }));
1199
+ const newPlan = {
1200
+ goal: state.goal,
1201
+ steps: newSteps,
1202
+ estimatedTokens: newSteps.length * 10_000,
1203
+ };
1204
+ // Re-initialize PlanGraph for the new plan
1205
+ this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, newPlan);
1206
+ this._logger.info("system", `PlanGraph re-initialized (replan): ${newSteps.length} nodes`);
1207
+ // Checkpoint after replan completes (save new plan state)
1208
+ await this.checkpoint(state.currentStepIndex);
1209
+ exitReplan();
1210
+ return newPlan;
1211
+ }
1212
+ // Fallback: single retry step
1213
+ this._logger.logReasoning("Hierarchical replanning unavailable, using single-step fallback");
1214
+ this.reflection.think("replan", "Using fallback single-step replan");
1215
+ const fallbackReplan = {
1216
+ goal: state.goal,
1217
+ steps: [
1218
+ {
1219
+ id: "replan-step-1",
1220
+ goal: `Retry: ${state.goal} (previous errors: ${state.errors.map((e) => e.error).join("; ")})`,
1221
+ targetFiles: [],
1222
+ readFiles: [],
1223
+ tools: ["file_read", "file_write", "file_edit", "grep", "glob"],
1224
+ estimatedIterations: 10,
1225
+ dependsOn: [],
1226
+ },
1227
+ ],
1228
+ estimatedTokens: 50_000,
1229
+ };
1230
+ // Re-initialize PlanGraph for fallback replan
1231
+ this.planGraph = PlanGraphManager.fromExecutionPlan(this.sessionId, fallbackReplan);
1232
+ this._logger.info("system", `PlanGraph re-initialized (fallback replan): 1 node`);
1233
+ // Checkpoint after fallback replan completes
1234
+ await this.checkpoint(state.currentStepIndex);
1235
+ exitReplan();
1236
+ return fallbackReplan;
1237
+ }
1238
+ /**
1239
+ * delegate phase 콜백 — 사용자에게 질문을 전달하고 응답을 대기한다.
1240
+ *
1241
+ * 현재 구현에서는 기본 응답을 반환한다.
1242
+ * 실제 환경에서는 이벤트를 emit하고 외부 응답을 대기해야 한다.
1243
+ *
1244
+ * @param question - 사용자에게 전달할 질문
1245
+ * @returns 사용자 응답
1246
+ */
1247
+ async delegateToUser(question) {
1248
+ this.reflection.think("delegate", `Delegating to user: "${question}"`);
1249
+ // Emit event for external handling
1250
+ this.emit("engine:delegate", question);
1251
+ // In a real implementation, this would wait for user input.
1252
+ // For now, return a default that selects the recommended approach.
1253
+ return "1";
1254
+ }
1255
+ // ─── Helpers ───────────────────────────────────────────────────
1256
+ /**
1257
+ * 변경 전 파일 내용을 스냅샷으로 저장한다.
1258
+ * 이미 스냅샷이 있는 파일은 건너뛴다 (최초 원본만 보존).
1259
+ *
1260
+ * @param files - 스냅샷할 파일 경로 목록
1261
+ */
1262
+ async snapshotFiles(files) {
1263
+ for (const file of files) {
1264
+ if (this.originalFiles.has(file))
1265
+ continue;
1266
+ try {
1267
+ const content = await readFile(file, "utf-8");
1268
+ this.originalFiles.set(file, content);
1269
+ }
1270
+ catch {
1271
+ // File doesn't exist yet — no snapshot needed
1272
+ }
1273
+ }
1274
+ }
1275
+ /**
1276
+ * 개별 step 실행을 위한 AgentLoop 인스턴스를 생성한다.
1277
+ *
1278
+ * @param systemPrompt - step별 시스템 프롬프트
1279
+ * @returns 새 AgentLoop 인스턴스
1280
+ */
1281
+ createAgentLoop(systemPrompt) {
1282
+ // Merge base tool definitions with MCP tool definitions
1283
+ const tools = [
1284
+ ...this.config.toolExecutor.definitions,
1285
+ ...this.mcpToolDefinitions,
1286
+ ];
1287
+ // Wrap tool executor to add sandbox validation and MCP tool routing
1288
+ const baseExecutor = this.config.toolExecutor;
1289
+ const sandboxMgr = this.sandboxManager;
1290
+ const mcpCli = this.mcpClient;
1291
+ const mcpToolDefs = this.mcpToolDefinitions;
1292
+ const wrappedExecutor = {
1293
+ definitions: tools,
1294
+ execute: async (call, abortSignal) => {
1295
+ // Parse arguments to object if needed
1296
+ let args;
1297
+ try {
1298
+ args = typeof call.arguments === "string"
1299
+ ? JSON.parse(call.arguments)
1300
+ : call.arguments;
1301
+ }
1302
+ catch {
1303
+ return {
1304
+ tool_call_id: call.id,
1305
+ name: call.name,
1306
+ output: `[Error] Invalid JSON in tool arguments: ${String(call.arguments).slice(0, 200)}`,
1307
+ success: false,
1308
+ durationMs: 0,
1309
+ };
1310
+ }
1311
+ // Sandbox pre-validation
1312
+ if (sandboxMgr) {
1313
+ const validation = sandboxMgr.validateToolCall(call.name, args);
1314
+ if (!validation.allowed) {
1315
+ return {
1316
+ tool_call_id: call.id,
1317
+ name: call.name,
1318
+ output: `[Sandbox blocked] ${validation.violations.join("; ")}`,
1319
+ success: false,
1320
+ durationMs: 0,
1321
+ };
1322
+ }
1323
+ }
1324
+ // Route MCP tool calls to MCPClient (check MCP registry, not string pattern)
1325
+ if (mcpCli && mcpToolDefs.some((t) => t.name === call.name)) {
1326
+ return mcpCli.callToolAsYuan(call.name, args, call.id);
1327
+ }
1328
+ // Default: use base executor
1329
+ return baseExecutor.execute(call, abortSignal);
1330
+ },
1331
+ };
1332
+ return new AgentLoop({
1333
+ config: {
1334
+ byok: this.config.byokConfig,
1335
+ loop: {
1336
+ model: "coding",
1337
+ maxIterations: this.config.maxIterations,
1338
+ maxTokensPerIteration: 16_384,
1339
+ totalTokenBudget: this.config.totalTokenBudget,
1340
+ tools,
1341
+ systemPrompt,
1342
+ projectPath: this.config.projectPath,
1343
+ },
1344
+ },
1345
+ toolExecutor: wrappedExecutor,
1346
+ governorConfig: this.config.governorConfig ?? { planTier: "PRO" },
1347
+ contextConfig: this.config.contextConfig,
1348
+ approvalConfig: this.config.approvalConfig,
1349
+ approvalHandler: this.config.approvalHandler,
1350
+ autoFixConfig: this.config.autoFixConfig,
1351
+ });
1352
+ }
1353
+ /**
1354
+ * AgentLoop 이벤트를 ExecutionEngine 이벤트로 전파한다.
1355
+ *
1356
+ * @param loop - 이벤트를 연결할 AgentLoop 인스턴스
1357
+ */
1358
+ wireAgentLoopEvents(loop) {
1359
+ loop.on("event", (event) => {
1360
+ switch (event.kind) {
1361
+ case "agent:text_delta":
1362
+ this.emit("text_delta", event.text);
1363
+ break;
1364
+ case "agent:thinking":
1365
+ this.emit("thinking", event.content);
1366
+ break;
1367
+ case "agent:tool_call": {
1368
+ const toolName = event.tool;
1369
+ const toolInput = (event.input ?? {});
1370
+ // Sandbox pre-validation: block disallowed tool calls
1371
+ if (this.sandboxManager) {
1372
+ const validation = this.sandboxManager.validateToolCall(toolName, toolInput);
1373
+ if (!validation.allowed) {
1374
+ this._logger.warn("system", `Sandbox blocked tool "${toolName}": ${validation.violations.join("; ")}`);
1375
+ // The event is still emitted for observability, but the
1376
+ // AgentLoop's toolExecutor wrapper handles the actual blocking.
1377
+ // We emit a synthetic tool_result error to signal the block.
1378
+ }
1379
+ }
1380
+ this._logger.logToolCall(toolName, toolInput);
1381
+ this.emit("tool:call", toolName, toolInput);
1382
+ break;
1383
+ }
1384
+ case "agent:tool_result":
1385
+ this._logger.logToolResult(event.tool, event.output, event.durationMs ?? 0, event.success ?? true);
1386
+ this.emit("tool:result", event.tool, event.output);
1387
+ if (this.perfOptimizer) {
1388
+ this.perfOptimizer.recordToolCall(event.tool, event.input ?? {}, event.durationMs ?? 0);
1389
+ }
1390
+ break;
1391
+ case "agent:file_change":
1392
+ // Track changed files
1393
+ if (typeof event.path === "string") {
1394
+ this.changedFiles.add(event.path);
1395
+ }
1396
+ break;
1397
+ }
1398
+ });
1399
+ }
1400
+ /**
1401
+ * analyze phase용 분석 프롬프트를 생성한다.
1402
+ *
1403
+ * @param goal - 사용자 목표
1404
+ * @param codebaseStats - CodebaseContext 통계
1405
+ * @returns 시스템 프롬프트
1406
+ */
1407
+ buildAnalysisPrompt(goal, codebaseStats) {
1408
+ const statsStr = Object.keys(codebaseStats).length > 0
1409
+ ? `\n## Codebase Statistics\n${JSON.stringify(codebaseStats, null, 2)}`
1410
+ : "";
1411
+ return `You are a code analysis expert. Analyze the user's request and the project context.
1412
+ ${statsStr}
1413
+
1414
+ ## Instructions
1415
+ 1. Determine the complexity of the request: trivial, simple, moderate, complex, or massive
1416
+ 2. Identify relevant files and patterns
1417
+ 3. Note any risks or dependencies
1418
+ 4. Provide a brief analysis summary
1419
+
1420
+ Start your response with the complexity level (e.g., "Complexity: moderate")
1421
+ Then provide your analysis.`;
1422
+ }
1423
+ /**
1424
+ * 개별 step 실행을 위한 시스템 프롬프트를 생성한다.
1425
+ *
1426
+ * @param plan - 전체 실행 계획
1427
+ * @param step - 현재 step
1428
+ * @param stepIndex - step 인덱스
1429
+ * @returns 시스템 프롬프트
1430
+ */
1431
+ buildStepPrompt(plan, step, stepIndex) {
1432
+ const sections = [];
1433
+ sections.push(`You are an expert coding agent executing step ${stepIndex + 1} of ${plan.steps.length}.`);
1434
+ sections.push("");
1435
+ sections.push(`## Overall Goal`);
1436
+ sections.push(plan.goal);
1437
+ sections.push("");
1438
+ sections.push(`## Current Step`);
1439
+ sections.push(step.goal);
1440
+ sections.push("");
1441
+ if (step.targetFiles.length > 0) {
1442
+ sections.push(`## Target Files`);
1443
+ sections.push(step.targetFiles.join(", "));
1444
+ sections.push("");
1445
+ }
1446
+ if (step.readFiles.length > 0) {
1447
+ sections.push(`## Reference Files (read only)`);
1448
+ sections.push(step.readFiles.join(", "));
1449
+ sections.push("");
1450
+ }
1451
+ sections.push(`## Instructions`);
1452
+ sections.push(`- Focus on this step's goal only`);
1453
+ sections.push(`- Read relevant files before making changes`);
1454
+ sections.push(`- Make precise, minimal changes`);
1455
+ sections.push(`- Verify your changes compile/work`);
1456
+ sections.push(`- When done, provide a summary of what you changed`);
1457
+ return sections.join("\n");
1458
+ }
1459
+ /**
1460
+ * DeepVerifyResult를 StateMachine의 VerifyResult 형식으로 변환한다.
1461
+ *
1462
+ * @param deep - 6-dimension 심층 검증 결과
1463
+ * @returns StateMachine VerifyResult
1464
+ */
1465
+ mapVerifyResult(deep) {
1466
+ const d = deep.dimensions;
1467
+ return {
1468
+ verdict: deep.verdict,
1469
+ checks: {
1470
+ buildSuccess: d.correctness.status !== "fail",
1471
+ typesSafe: d.correctness.status !== "fail",
1472
+ testsPass: d.completeness.status !== "fail",
1473
+ noRegressions: d.consistency.status !== "fail",
1474
+ followsPatterns: d.quality.status !== "fail",
1475
+ securityClean: d.security.status !== "fail",
1476
+ },
1477
+ issues: [
1478
+ ...d.correctness.issues,
1479
+ ...d.completeness.issues,
1480
+ ...d.consistency.issues,
1481
+ ...d.quality.issues,
1482
+ ...d.security.issues,
1483
+ ...d.performance.issues,
1484
+ ],
1485
+ suggestions: deep.suggestedFixes.map((f) => `[${f.dimension}/${f.severity}] ${f.description}`),
1486
+ confidence: deep.confidence,
1487
+ };
1488
+ }
1489
+ /**
1490
+ * StateMachine 최종 상태에서 ExecutionResult를 생성한다.
1491
+ *
1492
+ * @param smState - StateMachine 최종 상태
1493
+ * @param startTime - 실행 시작 시각 (epoch ms)
1494
+ * @returns 실행 결과
1495
+ */
1496
+ buildResult(smState, startTime) {
1497
+ const hasErrors = smState.errors.length > 0;
1498
+ const lastReflection = smState.reflections[smState.reflections.length - 1];
1499
+ const passed = !hasErrors ||
1500
+ (lastReflection && lastReflection.verdict === "pass");
1501
+ // Build summary from step results
1502
+ const summaryParts = [];
1503
+ for (const sr of smState.stepResults) {
1504
+ if (sr.phase === "implement" || sr.phase === "fix") {
1505
+ summaryParts.push(`[${sr.phase}] ${sr.output}`);
1506
+ }
1507
+ }
1508
+ const summary = summaryParts.length > 0
1509
+ ? summaryParts.join("\n")
1510
+ : `Execution completed in phase: ${smState.phase}`;
1511
+ return {
1512
+ success: !!passed,
1513
+ termination: this.lastTermination,
1514
+ finalPhase: smState.phase,
1515
+ changedFiles: [...this.changedFiles],
1516
+ summary,
1517
+ totalTokens: {
1518
+ input: smState.tokenUsage.input,
1519
+ output: smState.tokenUsage.output,
1520
+ },
1521
+ totalIterations: smState.iterationCount,
1522
+ totalToolCalls: smState.toolCalls,
1523
+ durationMs: Date.now() - startTime,
1524
+ verifyResult: this.lastVerifyResult,
1525
+ monologue: [...this.reflection.getMonologue()],
1526
+ learnings: [...this.reflection.getAllLearnings()],
1527
+ plan: this.hierarchicalPlan,
1528
+ };
1529
+ }
1530
+ /**
1531
+ * JSON 문자열에서 JSON 블록을 추출한다.
1532
+ * markdown 코드 펜스 또는 순수 JSON을 처리한다.
1533
+ *
1534
+ * @param content - 원본 문자열
1535
+ * @returns 추출된 JSON 문자열
1536
+ */
1537
+ extractJson(content) {
1538
+ // Strip markdown code fences
1539
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
1540
+ if (fenced) {
1541
+ return fenced[1].trim();
1542
+ }
1543
+ // Find first JSON array or object
1544
+ const arrayMatch = content.match(/\[[\s\S]*\]/);
1545
+ if (arrayMatch) {
1546
+ return arrayMatch[0];
1547
+ }
1548
+ const objectMatch = content.match(/\{[\s\S]*\}/);
1549
+ if (objectMatch) {
1550
+ return objectMatch[0];
1551
+ }
1552
+ return content.trim();
1553
+ }
1554
+ }
1555
+ //# sourceMappingURL=execution-engine.js.map