@yuaone/core 0.9.8 → 0.9.9

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 (196) hide show
  1. package/dist/__tests__/context-manager.test.js +5 -9
  2. package/dist/__tests__/context-manager.test.js.map +1 -1
  3. package/dist/agent-coordinator.d.ts +172 -0
  4. package/dist/agent-coordinator.d.ts.map +1 -0
  5. package/dist/agent-coordinator.js +390 -0
  6. package/dist/agent-coordinator.js.map +1 -0
  7. package/dist/agent-loop.d.ts +83 -39
  8. package/dist/agent-loop.d.ts.map +1 -1
  9. package/dist/agent-loop.js +694 -471
  10. package/dist/agent-loop.js.map +1 -1
  11. package/dist/agent-reputation.d.ts +72 -0
  12. package/dist/agent-reputation.d.ts.map +1 -0
  13. package/dist/agent-reputation.js +222 -0
  14. package/dist/agent-reputation.js.map +1 -0
  15. package/dist/arch-summarizer.d.ts +48 -0
  16. package/dist/arch-summarizer.d.ts.map +1 -0
  17. package/dist/arch-summarizer.js +239 -0
  18. package/dist/arch-summarizer.js.map +1 -0
  19. package/dist/autonomous/explicit-planner.d.ts +45 -0
  20. package/dist/autonomous/explicit-planner.d.ts.map +1 -0
  21. package/dist/autonomous/explicit-planner.js +99 -0
  22. package/dist/autonomous/explicit-planner.js.map +1 -0
  23. package/dist/autonomous/incident-debugger.d.ts +78 -0
  24. package/dist/autonomous/incident-debugger.d.ts.map +1 -0
  25. package/dist/autonomous/incident-debugger.js +324 -0
  26. package/dist/autonomous/incident-debugger.js.map +1 -0
  27. package/dist/autonomous/index.d.ts +15 -0
  28. package/dist/autonomous/index.d.ts.map +1 -0
  29. package/dist/autonomous/index.js +10 -0
  30. package/dist/autonomous/index.js.map +1 -0
  31. package/dist/autonomous/patch-tournament.d.ts +82 -0
  32. package/dist/autonomous/patch-tournament.d.ts.map +1 -0
  33. package/dist/autonomous/patch-tournament.js +150 -0
  34. package/dist/autonomous/patch-tournament.js.map +1 -0
  35. package/dist/autonomous/research-agent.d.ts +66 -0
  36. package/dist/autonomous/research-agent.d.ts.map +1 -0
  37. package/dist/autonomous/research-agent.js +210 -0
  38. package/dist/autonomous/research-agent.js.map +1 -0
  39. package/dist/autonomous/task-memory.d.ts +63 -0
  40. package/dist/autonomous/task-memory.d.ts.map +1 -0
  41. package/dist/autonomous/task-memory.js +143 -0
  42. package/dist/autonomous/task-memory.js.map +1 -0
  43. package/dist/budget-governor-v2.d.ts +93 -0
  44. package/dist/budget-governor-v2.d.ts.map +1 -0
  45. package/dist/budget-governor-v2.js +345 -0
  46. package/dist/budget-governor-v2.js.map +1 -0
  47. package/dist/capability-graph.d.ts +102 -0
  48. package/dist/capability-graph.d.ts.map +1 -0
  49. package/dist/capability-graph.js +397 -0
  50. package/dist/capability-graph.js.map +1 -0
  51. package/dist/capability-self-model.d.ts +144 -0
  52. package/dist/capability-self-model.d.ts.map +1 -0
  53. package/dist/capability-self-model.js +312 -0
  54. package/dist/capability-self-model.js.map +1 -0
  55. package/dist/checkpoint-manager.d.ts +94 -0
  56. package/dist/checkpoint-manager.d.ts.map +1 -0
  57. package/dist/checkpoint-manager.js +225 -0
  58. package/dist/checkpoint-manager.js.map +1 -0
  59. package/dist/continuation-engine.js +1 -1
  60. package/dist/continuation-engine.js.map +1 -1
  61. package/dist/dag-orchestrator.d.ts +0 -3
  62. package/dist/dag-orchestrator.d.ts.map +1 -1
  63. package/dist/dag-orchestrator.js +0 -1
  64. package/dist/dag-orchestrator.js.map +1 -1
  65. package/dist/evidence-chain.d.ts +99 -0
  66. package/dist/evidence-chain.d.ts.map +1 -0
  67. package/dist/evidence-chain.js +200 -0
  68. package/dist/evidence-chain.js.map +1 -0
  69. package/dist/execution-engine.d.ts.map +1 -1
  70. package/dist/execution-engine.js +0 -1
  71. package/dist/execution-engine.js.map +1 -1
  72. package/dist/failure-signature-memory.d.ts +61 -0
  73. package/dist/failure-signature-memory.d.ts.map +1 -0
  74. package/dist/failure-signature-memory.js +278 -0
  75. package/dist/failure-signature-memory.js.map +1 -0
  76. package/dist/index.d.ts +52 -5
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +48 -7
  79. package/dist/index.js.map +1 -1
  80. package/dist/language-detector.d.ts.map +1 -1
  81. package/dist/language-detector.js +122 -43
  82. package/dist/language-detector.js.map +1 -1
  83. package/dist/llm-client.d.ts +0 -7
  84. package/dist/llm-client.d.ts.map +1 -1
  85. package/dist/llm-client.js +15 -122
  86. package/dist/llm-client.js.map +1 -1
  87. package/dist/mcp-client.js +1 -1
  88. package/dist/mcp-client.js.map +1 -1
  89. package/dist/memory.d.ts.map +1 -1
  90. package/dist/memory.js +0 -15
  91. package/dist/memory.js.map +1 -1
  92. package/dist/meta-learning-collector.d.ts +64 -0
  93. package/dist/meta-learning-collector.d.ts.map +1 -0
  94. package/dist/meta-learning-collector.js +169 -0
  95. package/dist/meta-learning-collector.js.map +1 -0
  96. package/dist/meta-learning-engine.d.ts +61 -0
  97. package/dist/meta-learning-engine.d.ts.map +1 -0
  98. package/dist/meta-learning-engine.js +250 -0
  99. package/dist/meta-learning-engine.js.map +1 -0
  100. package/dist/overhead-governor.d.ts +105 -0
  101. package/dist/overhead-governor.d.ts.map +1 -0
  102. package/dist/overhead-governor.js +239 -0
  103. package/dist/overhead-governor.js.map +1 -0
  104. package/dist/playbook-library.d.ts +75 -0
  105. package/dist/playbook-library.d.ts.map +1 -0
  106. package/dist/playbook-library.js +241 -0
  107. package/dist/playbook-library.js.map +1 -0
  108. package/dist/project-executive.d.ts +97 -0
  109. package/dist/project-executive.d.ts.map +1 -0
  110. package/dist/project-executive.js +223 -0
  111. package/dist/project-executive.js.map +1 -0
  112. package/dist/reasoning-adapter.d.ts.map +1 -1
  113. package/dist/reasoning-adapter.js +11 -36
  114. package/dist/reasoning-adapter.js.map +1 -1
  115. package/dist/research-loop.d.ts +79 -0
  116. package/dist/research-loop.d.ts.map +1 -0
  117. package/dist/research-loop.js +363 -0
  118. package/dist/research-loop.js.map +1 -0
  119. package/dist/resolve-memory-path.d.ts +32 -0
  120. package/dist/resolve-memory-path.d.ts.map +1 -0
  121. package/dist/resolve-memory-path.js +97 -0
  122. package/dist/resolve-memory-path.js.map +1 -0
  123. package/dist/safe-bounds.d.ts +101 -0
  124. package/dist/safe-bounds.d.ts.map +1 -0
  125. package/dist/safe-bounds.js +140 -0
  126. package/dist/safe-bounds.js.map +1 -0
  127. package/dist/sandbox-tiers.d.ts +5 -0
  128. package/dist/sandbox-tiers.d.ts.map +1 -1
  129. package/dist/sandbox-tiers.js +14 -6
  130. package/dist/sandbox-tiers.js.map +1 -1
  131. package/dist/security.d.ts.map +1 -1
  132. package/dist/security.js +3 -0
  133. package/dist/security.js.map +1 -1
  134. package/dist/self-improvement-loop.d.ts +64 -0
  135. package/dist/self-improvement-loop.d.ts.map +1 -0
  136. package/dist/self-improvement-loop.js +156 -0
  137. package/dist/self-improvement-loop.js.map +1 -0
  138. package/dist/session-persistence.d.ts +5 -0
  139. package/dist/session-persistence.d.ts.map +1 -1
  140. package/dist/session-persistence.js +19 -3
  141. package/dist/session-persistence.js.map +1 -1
  142. package/dist/skill-loader.d.ts +16 -9
  143. package/dist/skill-loader.d.ts.map +1 -1
  144. package/dist/skill-loader.js +52 -116
  145. package/dist/skill-loader.js.map +1 -1
  146. package/dist/skill-registry.d.ts +60 -0
  147. package/dist/skill-registry.d.ts.map +1 -0
  148. package/dist/skill-registry.js +162 -0
  149. package/dist/skill-registry.js.map +1 -0
  150. package/dist/stall-detector.d.ts +56 -0
  151. package/dist/stall-detector.d.ts.map +1 -0
  152. package/dist/stall-detector.js +142 -0
  153. package/dist/stall-detector.js.map +1 -0
  154. package/dist/strategy-learner.d.ts +57 -0
  155. package/dist/strategy-learner.d.ts.map +1 -0
  156. package/dist/strategy-learner.js +160 -0
  157. package/dist/strategy-learner.js.map +1 -0
  158. package/dist/strategy-market.d.ts +73 -0
  159. package/dist/strategy-market.d.ts.map +1 -0
  160. package/dist/strategy-market.js +200 -0
  161. package/dist/strategy-market.js.map +1 -0
  162. package/dist/sub-agent.d.ts +0 -3
  163. package/dist/sub-agent.d.ts.map +1 -1
  164. package/dist/sub-agent.js +0 -10
  165. package/dist/sub-agent.js.map +1 -1
  166. package/dist/system-prompt.d.ts +0 -2
  167. package/dist/system-prompt.d.ts.map +1 -1
  168. package/dist/system-prompt.js +97 -490
  169. package/dist/system-prompt.js.map +1 -1
  170. package/dist/task-classifier.d.ts.map +1 -1
  171. package/dist/task-classifier.js +2 -54
  172. package/dist/task-classifier.js.map +1 -1
  173. package/dist/tool-synthesizer.d.ts +149 -0
  174. package/dist/tool-synthesizer.d.ts.map +1 -0
  175. package/dist/tool-synthesizer.js +455 -0
  176. package/dist/tool-synthesizer.js.map +1 -0
  177. package/dist/trace-pattern-extractor.d.ts +76 -0
  178. package/dist/trace-pattern-extractor.d.ts.map +1 -0
  179. package/dist/trace-pattern-extractor.js +321 -0
  180. package/dist/trace-pattern-extractor.js.map +1 -0
  181. package/dist/trace-recorder.d.ts +38 -0
  182. package/dist/trace-recorder.d.ts.map +1 -0
  183. package/dist/trace-recorder.js +94 -0
  184. package/dist/trace-recorder.js.map +1 -0
  185. package/dist/trust-economics.d.ts +50 -0
  186. package/dist/trust-economics.d.ts.map +1 -0
  187. package/dist/trust-economics.js +148 -0
  188. package/dist/trust-economics.js.map +1 -0
  189. package/dist/types.d.ts +273 -4
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/types.js.map +1 -1
  192. package/dist/yuan-md-loader.d.ts +22 -0
  193. package/dist/yuan-md-loader.d.ts.map +1 -0
  194. package/dist/yuan-md-loader.js +75 -0
  195. package/dist/yuan-md-loader.js.map +1 -0
  196. package/package.json +1 -1
@@ -9,17 +9,7 @@
9
9
  import { EventEmitter } from "node:events";
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { execSync } from "node:child_process";
12
- import { appendFileSync, mkdirSync } from "node:fs";
13
- import { homedir } from "node:os";
14
12
  import { basename, join as pathJoin } from "node:path";
15
- function debugLog(msg) {
16
- try {
17
- const dir = `${homedir()}/.yuan/logs`;
18
- mkdirSync(dir, { recursive: true });
19
- appendFileSync(`${dir}/debug.log`, `[${new Date().toISOString()}] ${msg}\n`);
20
- }
21
- catch { /* silent */ }
22
- }
23
13
  import { BYOKClient } from "./llm-client.js";
24
14
  import { Governor } from "./governor.js";
25
15
  import { ContextManager } from "./context-manager.js";
@@ -31,7 +21,6 @@ import { InterruptManager } from "./interrupt-manager.js";
31
21
  import { YuanMemory } from "./memory.js";
32
22
  import { MemoryManager } from "./memory-manager.js";
33
23
  import { buildSystemPrompt } from "./system-prompt.js";
34
- import { StrategySelector } from "./strategy-selector.js";
35
24
  import { HierarchicalPlanner, } from "./hierarchical-planner.js";
36
25
  import { TaskClassifier } from "./task-classifier.js";
37
26
  import { PromptDefense } from "./prompt-defense.js";
@@ -40,6 +29,7 @@ import { TokenBudgetManager } from "./token-budget.js";
40
29
  import { ContinuationEngine } from "./continuation-engine.js";
41
30
  import { MemoryUpdater } from "./memory-updater.js";
42
31
  import { MCPClient } from "./mcp-client.js";
32
+ import { BOUNDS, cap } from "./safe-bounds.js";
43
33
  import { WorldStateCollector } from "./world-state.js";
44
34
  import { FailureRecovery } from "./failure-recovery.js";
45
35
  import { ExecutionPolicyEngine } from "./execution-policy-engine.js";
@@ -65,12 +55,27 @@ import { ContextBudgetManager } from "./context-budget.js";
65
55
  import { QAPipeline } from "./qa-pipeline.js";
66
56
  import { PersonaManager } from "./persona.js";
67
57
  import { InMemoryVectorStore, OllamaEmbeddingProvider } from "./vector-store.js";
58
+ import { OverheadGovernor } from "./overhead-governor.js";
68
59
  import { StateStore, TransitionModel, SimulationEngine, StateUpdater, } from "./world-model/index.js";
69
60
  import { MilestoneChecker, RiskEstimator, PlanEvaluator, ReplanningEngine, } from "./planner/index.js";
70
- import { SandboxManager } from "./sandbox-tiers.js";
71
- import { SecurityScanner } from "./security-scanner.js";
72
- import { ReasoningProgressAdapter } from "./reasoning-adapter.js";
73
- import { GitIntelligence } from "./git-intelligence.js";
61
+ import { TraceRecorder } from "./trace-recorder.js";
62
+ import { ArchSummarizer } from "./arch-summarizer.js";
63
+ import { FailureSignatureMemory } from "./failure-signature-memory.js";
64
+ import { PlaybookLibrary } from "./playbook-library.js";
65
+ import { ProjectExecutive } from "./project-executive.js";
66
+ import { StallDetector } from "./stall-detector.js";
67
+ import { SelfImprovementLoop } from "./self-improvement-loop.js";
68
+ import { MetaLearningCollector } from "./meta-learning-collector.js";
69
+ import { TrustEconomics } from "./trust-economics.js";
70
+ import { StrategyLearner } from "./strategy-learner.js";
71
+ import { SkillRegistry } from "./skill-registry.js";
72
+ import { TracePatternExtractor } from "./trace-pattern-extractor.js";
73
+ import { MetaLearningEngine } from "./meta-learning-engine.js";
74
+ import { ToolSynthesizer } from "./tool-synthesizer.js";
75
+ import { BudgetGovernorV2 } from "./budget-governor-v2.js";
76
+ import { CapabilityGraph } from "./capability-graph.js";
77
+ import { CapabilitySelfModel } from "./capability-self-model.js";
78
+ import { StrategyMarket } from "./strategy-market.js";
74
79
  /**
75
80
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
76
81
  *
@@ -100,87 +105,6 @@ import { GitIntelligence } from "./git-intelligence.js";
100
105
  */
101
106
  /** Minimum confidence for classification-based hints/routing to activate */
102
107
  const CLASSIFICATION_CONFIDENCE_THRESHOLD = 0.6;
103
- // ─── Security Table Formatter ─────────────────────────────────────────────────
104
- /**
105
- * Renders SecurityScanner findings as a box-drawn terminal table with ANSI colors.
106
- *
107
- * ⚠ Security Scan · src/app.ts
108
- * ┌────────────┬─────────┬──────────────────────────────────┐
109
- * │ Severity │ Count │ Rules │
110
- * ├────────────┼─────────┼──────────────────────────────────┤
111
- * │ CRITICAL │ 1 │ hardcoded-secret │
112
- * │ HIGH │ 2 │ sql-injection, xss-reflected │
113
- * │ MEDIUM │ 0 │ — │
114
- * │ LOW │ 1 │ weak-hash │
115
- * └────────────┴─────────┴──────────────────────────────────┘
116
- */
117
- function formatSecurityTable(findings, filePath) {
118
- // ANSI
119
- const R = "\x1b[0m";
120
- const BOLD = "\x1b[1m";
121
- const DIM = "\x1b[2m";
122
- const RED = "\x1b[1;31m";
123
- const YEL = "\x1b[33m";
124
- const CYN = "\x1b[36m";
125
- const GRN = "\x1b[32m";
126
- const WHT = "\x1b[1;37m";
127
- // Aggregate
128
- const counts = { critical: 0, high: 0, medium: 0, low: 0 };
129
- const ruleMap = { critical: [], high: [], medium: [], low: [] };
130
- for (const f of findings) {
131
- const sev = f.severity.toLowerCase();
132
- if (sev in counts) {
133
- counts[sev]++;
134
- if (sev in ruleMap && !ruleMap[sev].includes(f.rule))
135
- ruleMap[sev].push(f.rule);
136
- }
137
- }
138
- const totalBad = counts.critical + counts.high;
139
- // Column widths (content only, borders + spaces added separately)
140
- const C1 = 10; // severity label
141
- const C2 = 7; // count
142
- const C3 = 34; // rules
143
- const h = "─";
144
- const v = "│";
145
- const tl = "┌";
146
- const tr = "┐";
147
- const bl = "└";
148
- const br = "┘";
149
- const lm = "├";
150
- const rm = "┤";
151
- const tm = "┬";
152
- const bm = "┴";
153
- const x = "┼";
154
- const divider = (L, M, Ri) => `${L}${h.repeat(C1 + 2)}${M}${h.repeat(C2 + 2)}${M}${h.repeat(C3 + 2)}${Ri}`;
155
- const row = (label, color, count, rules) => {
156
- const rulesStr = rules.length > 0 ? rules.join(", ") : "—";
157
- const col1 = label.padEnd(C1);
158
- const col2 = String(count).padStart(3).padEnd(C2);
159
- const col3 = (rulesStr.length > C3 ? rulesStr.slice(0, C3 - 1) + "…" : rulesStr).padEnd(C3);
160
- const c = count > 0 ? color : DIM;
161
- return `${v} ${c}${col1}${R} ${v} ${c}${col2}${R} ${v} ${c}${col3}${R} ${v}`;
162
- };
163
- const headerCol1 = "Severity".padEnd(C1);
164
- const headerCol2 = "Count".padEnd(C2);
165
- const headerCol3 = "Rules".padEnd(C3);
166
- const header = `${v} ${WHT}${headerCol1}${R} ${v} ${WHT}${headerCol2}${R} ${v} ${WHT}${headerCol3}${R} ${v}`;
167
- const shortPath = filePath.length > 44 ? "…" + filePath.slice(-43) : filePath;
168
- const icon = totalBad > 0 ? `${YEL}⚠${R}` : `${GRN}✓${R}`;
169
- return [
170
- "",
171
- `${icon} ${BOLD}Security Scan${R} ${DIM}· ${shortPath}${R}`,
172
- "",
173
- divider(tl, tm, tr),
174
- header,
175
- divider(lm, x, rm),
176
- row("CRITICAL", RED, counts.critical, ruleMap.critical),
177
- row("HIGH", YEL, counts.high, ruleMap.high),
178
- row("MEDIUM", CYN, counts.medium, ruleMap.medium),
179
- row("LOW", DIM, counts.low, ruleMap.low),
180
- divider(bl, bm, br),
181
- "",
182
- ].join("\n");
183
- }
184
108
  export class AgentLoop extends EventEmitter {
185
109
  abortSignal;
186
110
  llmClient;
@@ -208,10 +132,7 @@ export class AgentLoop extends EventEmitter {
208
132
  _lastInjectedTaskIndex = -1; // track when plan progress was last injected
209
133
  changedFiles = [];
210
134
  aborted = false;
211
- fastInitialized = false;
212
- slowInitDone = false;
213
- slowInitPromise = null;
214
- _slowInitContext = {};
135
+ initialized = false;
215
136
  continuationEngine = null;
216
137
  mcpClient = null;
217
138
  mcpToolDefinitions = [];
@@ -242,12 +163,6 @@ export class AgentLoop extends EventEmitter {
242
163
  skillLearner = null;
243
164
  repoGraph = null;
244
165
  backgroundAgentManager = null;
245
- sandbox = null;
246
- securityScanner = null;
247
- reasoningAdapter = null;
248
- gitIntelligence = null;
249
- gitAvailable = false;
250
- conflictCache = null;
251
166
  sessionPersistence = null;
252
167
  sessionId = null;
253
168
  enableToolPlanning;
@@ -255,9 +170,6 @@ export class AgentLoop extends EventEmitter {
255
170
  enableBackgroundAgents;
256
171
  currentToolPlan = null;
257
172
  executedToolNames = [];
258
- currentTaskType = undefined;
259
- strategySelector = new StrategySelector();
260
- activeStrategies = [];
261
173
  /** Context Budget: max 3 active skills at once */
262
174
  activeSkillIds = [];
263
175
  static MAX_ACTIVE_SKILLS = 3;
@@ -275,6 +187,22 @@ export class AgentLoop extends EventEmitter {
275
187
  iterationTsFilesModified = [];
276
188
  /** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
277
189
  tscRanLastIteration = false;
190
+ // ─── OverheadGovernor — subsystem execution policy ──────────────────────
191
+ overheadGovernor;
192
+ /** Writes since last verify ran (for QA/quickVerify trigger) */
193
+ writeCountSinceVerify = 0;
194
+ /** Current task phase (explore → implement → verify → finalize) */
195
+ taskPhase = "explore";
196
+ /** Per-iteration: verify already ran this iteration (single-flight guard) */
197
+ verifyRanThisIteration = false;
198
+ /** Per-iteration: summarize already ran this iteration */
199
+ summarizeRanThisIteration = false;
200
+ /** Per-iteration: llmFixer run count */
201
+ llmFixerRunCount = 0;
202
+ /** Repeated error signature (same error string seen 2+ times) */
203
+ repeatedErrorSignature = undefined;
204
+ _lastErrorSignature = undefined;
205
+ _errorSignatureCount = 0;
278
206
  /** PersonaManager — learns user communication style, injects persona into system prompt */
279
207
  personaManager = null;
280
208
  /** InMemoryVectorStore — RAG: semantic code context retrieval for relevant snippets */
@@ -303,6 +231,28 @@ export class AgentLoop extends EventEmitter {
303
231
  reasoningAggregator = new ReasoningAggregator();
304
232
  reasoningTree = new ReasoningTree();
305
233
  resumedFromSession = false;
234
+ traceRecorder = null;
235
+ archSummarizer = null;
236
+ failureSigMemory = null;
237
+ playbookLibrary = null;
238
+ projectExecutive = null;
239
+ stallDetector = null;
240
+ selfImprovementLoop = null;
241
+ metaLearningCollector = null;
242
+ trustEconomics = null;
243
+ // Phase 5: Strategy Learner + Skill Registry
244
+ strategyLearner = null;
245
+ skillRegistry = null;
246
+ // Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
247
+ tracePatternExtractor = null;
248
+ metaLearningEngine = null;
249
+ toolSynthesizer = null;
250
+ // Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
251
+ budgetGovernorV2 = null;
252
+ capabilityGraph = null;
253
+ capabilitySelfModel = null;
254
+ strategyMarket = null;
255
+ sessionRunCount = 0;
306
256
  /**
307
257
  * Restore AgentLoop state from persisted session (yuan resume)
308
258
  */
@@ -366,7 +316,7 @@ export class AgentLoop extends EventEmitter {
366
316
  this.abortSignal = options.abortSignal;
367
317
  this.enableMemory = options.enableMemory !== false;
368
318
  this.enablePlanning = options.enablePlanning !== false;
369
- this.planningThreshold = options.planningThreshold ?? "complex";
319
+ this.planningThreshold = options.planningThreshold ?? "moderate";
370
320
  this.environment = options.environment;
371
321
  this.enableSelfReflection = options.enableSelfReflection !== false;
372
322
  this.enableDebate = options.enableDebate !== false;
@@ -374,6 +324,11 @@ export class AgentLoop extends EventEmitter {
374
324
  this.llmClient = new BYOKClient(options.config.byok);
375
325
  // Governor 생성
376
326
  this.governor = new Governor(options.governorConfig);
327
+ // OverheadGovernor — subsystem execution policy
328
+ this.overheadGovernor = new OverheadGovernor(options.overheadGovernorConfig, (subsystem, reason) => {
329
+ // Shadow log — emit as thinking event so TUI can see it (dimmed)
330
+ this.emitEvent({ kind: "agent:thinking", content: `[shadow] ${subsystem}: ${reason}` });
331
+ });
377
332
  // ContextManager 생성
378
333
  this.contextManager = new ContextManager({
379
334
  maxContextTokens: options.contextConfig?.maxContextTokens ??
@@ -413,31 +368,6 @@ export class AgentLoop extends EventEmitter {
413
368
  this.enableToolPlanning = options.enableToolPlanning !== false;
414
369
  this.enableSkillLearning = options.enableSkillLearning !== false;
415
370
  this.enableBackgroundAgents = options.enableBackgroundAgents === true;
416
- // ReasoningProgressAdapter — converts thinking deltas to progress:status events
417
- this.reasoningAdapter = new ReasoningProgressAdapter({
418
- agentId: options.config.loop.model ?? "coder",
419
- role: "coder",
420
- onThinking: (event) => this.emitEvent(event),
421
- onStatus: (event) => this.emitEvent(event),
422
- debounceMs: 800,
423
- });
424
- // GitIntelligence — auto-disabled if git not installed or not a git repo
425
- const gitProject = options.config.loop.projectPath;
426
- if (gitProject && options.enableGitIntelligence !== false) {
427
- this.gitIntelligence = new GitIntelligence({ projectPath: gitProject });
428
- }
429
- // SandboxManager — preflight guard (auto-instantiated from projectPath)
430
- const sandboxProject = options.sandboxConfig?.projectPath ?? options.config.loop.projectPath;
431
- if (sandboxProject) {
432
- this.sandbox = new SandboxManager({
433
- projectPath: sandboxProject,
434
- ...(options.sandboxConfig ?? {}),
435
- });
436
- }
437
- // SecurityScanner — postflight guard (opt-in)
438
- if (options.enableSecurityScan && sandboxProject) {
439
- this.securityScanner = new SecurityScanner({ projectPath: sandboxProject });
440
- }
441
371
  // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
442
372
  this.contextManager.addMessage({
443
373
  role: "system",
@@ -445,44 +375,20 @@ export class AgentLoop extends EventEmitter {
445
375
  });
446
376
  }
447
377
  /**
448
- * Fast init runs synchronously before LLM call (< 50ms).
449
- * Sets up ContextBudgetManager and a minimal system prompt.
450
- * Idempotent: subsequent calls are no-ops.
378
+ * Memory와 프로젝트 컨텍스트를 로드하여 시스템 프롬프트를 갱신.
379
+ * run() 호출 전에 호출하면 메모리가 자동으로 주입된다.
380
+ * 이미 초기화되었으면 스킵.
451
381
  */
452
- async fastInit() {
453
- if (this.fastInitialized)
382
+ async init() {
383
+ if (this.initialized)
454
384
  return;
455
- this.fastInitialized = true;
456
- // ContextBudgetManager must be ready before any LLM call
385
+ this.initialized = true;
386
+ // Task 1: Initialize ContextBudgetManager with the total token budget
457
387
  this.contextBudgetManager = new ContextBudgetManager({
458
388
  totalBudget: this.config.loop.totalTokenBudget,
459
389
  enableSummarization: true,
460
- summarizationThreshold: 0.60,
461
- });
462
- const projectPath = this.config.loop.projectPath;
463
- if (!projectPath)
464
- return;
465
- // Minimal system prompt — no project structure yet (slowInit provides it later)
466
- const basicPrompt = buildSystemPrompt({
467
- projectStructure: undefined,
468
- yuanMdContent: undefined,
469
- tools: [...this.config.loop.tools],
470
- projectPath,
471
- environment: this.environment,
472
- model: this.config.byok.model,
390
+ summarizationThreshold: 0.60, // trigger summarize() at 60% (before ContextCompressor at 70%)
473
391
  });
474
- this.contextManager.replaceSystemMessage(basicPrompt);
475
- debugLog("[fastInit] done — LLM ready");
476
- }
477
- /**
478
- * Slow init — background fire-and-forget.
479
- * Loads memory, world state, MCP, etc. and enriches system prompt for next turn.
480
- * Idempotent: subsequent calls are no-ops.
481
- */
482
- async slowInit() {
483
- if (this.slowInitDone)
484
- return;
485
- this.slowInitDone = true;
486
392
  const projectPath = this.config.loop.projectPath;
487
393
  if (!projectPath)
488
394
  return;
@@ -493,47 +399,12 @@ export class AgentLoop extends EventEmitter {
493
399
  catch {
494
400
  this.sessionPersistence = null;
495
401
  }
496
- // Session save (moved from run() into slowInit)
497
- if (this.sessionPersistence) {
498
- try {
499
- if (!this.sessionId) {
500
- this.sessionId = randomUUID();
501
- }
502
- const nowIso = new Date().toISOString();
503
- const snapshot = {
504
- id: this.sessionId,
505
- createdAt: nowIso,
506
- updatedAt: nowIso,
507
- workDir: projectPath,
508
- provider: String(this.config.byok.provider ?? "unknown"),
509
- model: String(this.config.byok.model ?? "unknown"),
510
- status: "running",
511
- iteration: this.iterationCount,
512
- tokenUsage: {
513
- input: this.tokenUsage.input,
514
- output: this.tokenUsage.output,
515
- },
516
- messageCount: this.contextManager.getMessages().length,
517
- };
518
- await this.sessionPersistence.save(this.sessionId, {
519
- snapshot,
520
- messages: this.contextManager.getMessages(),
521
- plan: this.activePlan,
522
- changedFiles: [],
523
- });
524
- }
525
- catch {
526
- // Session save failure is non-fatal
527
- }
528
- }
529
402
  let yuanMdContent;
530
403
  let projectStructure;
531
- debugLog("[slowInit] start");
532
404
  // Memory 로드
533
405
  if (this.enableMemory) {
534
406
  try {
535
407
  // YUAN.md (raw markdown)
536
- debugLog("[slowInit] yuanMemory.load start");
537
408
  this.yuanMemory = new YuanMemory(projectPath);
538
409
  const memData = await this.yuanMemory.load();
539
410
  if (memData) {
@@ -563,20 +434,8 @@ export class AgentLoop extends EventEmitter {
563
434
  const indexer = new CodeIndexer({});
564
435
  indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
565
436
  }).catch(() => { });
566
- // 프로젝트 구조 분석 (5초 타임아웃)
567
- debugLog("[slowInit] analyzeProject start");
568
- const analyzeTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("analyzeProject timeout")), 15000));
569
- projectStructure = await Promise.race([
570
- this.yuanMemory.analyzeProject(),
571
- analyzeTimeout,
572
- ]).catch((e) => {
573
- debugLog(`[slowInit] analyzeProject skipped: ${String(e)}`);
574
- return undefined;
575
- });
576
- debugLog("[slowInit] analyzeProject done");
577
- // Store for enriched system prompt at end of slowInit
578
- this._slowInitContext.yuanMdContent = yuanMdContent;
579
- this._slowInitContext.projectStructure = projectStructure;
437
+ // 프로젝트 구조 분석
438
+ projectStructure = await this.yuanMemory.analyzeProject();
580
439
  }
581
440
  catch (memErr) {
582
441
  // 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
@@ -603,7 +462,6 @@ export class AgentLoop extends EventEmitter {
603
462
  catch {
604
463
  // 정책 로드 실패 → 기본값 사용
605
464
  }
606
- debugLog("[slowInit] worldState.collect start");
607
465
  // WorldState 수집 → system prompt에 주입
608
466
  try {
609
467
  const worldStateCollector = new WorldStateCollector({
@@ -612,11 +470,9 @@ export class AgentLoop extends EventEmitter {
612
470
  skipTest: true,
613
471
  });
614
472
  this.worldState = await worldStateCollector.collect();
615
- this._slowInitContext.worldState = this.worldState ?? undefined;
616
- debugLog("[slowInit] worldState.collect done");
617
473
  }
618
- catch (e) {
619
- debugLog(`[slowInit] worldState.collect ERROR: ${String(e)}`);
474
+ catch {
475
+ // WorldState 수집 실패는 치명적이지 않음
620
476
  }
621
477
  // Initialize World Model
622
478
  if (this.worldState && projectPath) {
@@ -644,13 +500,11 @@ export class AgentLoop extends EventEmitter {
644
500
  catch {
645
501
  // Not a git repo or git unavailable — FailureRecovery will use file-level rollback only
646
502
  }
647
- debugLog("[slowInit] continuationEngine + MCP start");
648
503
  // ImpactAnalyzer 생성
649
504
  this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
650
505
  // ContinuationEngine 생성
651
506
  this.continuationEngine = new ContinuationEngine({ projectPath });
652
507
  // 이전 세션 체크포인트 복원
653
- debugLog("[slowInit] checkpoint restore start");
654
508
  try {
655
509
  const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
656
510
  if (latestCheckpoint) {
@@ -666,9 +520,7 @@ export class AgentLoop extends EventEmitter {
666
520
  catch {
667
521
  // 체크포인트 복원 실패는 치명적이지 않음
668
522
  }
669
- debugLog("[slowInit] checkpoint restore done");
670
523
  // MCP 클라이언트 연결
671
- debugLog("[slowInit] MCP connect start");
672
524
  if (this.mcpServerConfigs.length > 0) {
673
525
  try {
674
526
  this.mcpClient = new MCPClient({
@@ -683,7 +535,6 @@ export class AgentLoop extends EventEmitter {
683
535
  this.mcpToolDefinitions = [];
684
536
  }
685
537
  }
686
- debugLog("[slowInit] MCP connect done");
687
538
  // ReflexionEngine 생성
688
539
  if (projectPath) {
689
540
  this.reflexionEngine = new ReflexionEngine({ projectPath });
@@ -722,6 +573,22 @@ export class AgentLoop extends EventEmitter {
722
573
  totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
723
574
  });
724
575
  }
576
+ // 향상된 시스템 프롬프트 생성
577
+ const enhancedPrompt = buildSystemPrompt({
578
+ projectStructure,
579
+ yuanMdContent,
580
+ tools: [...this.config.loop.tools, ...this.mcpToolDefinitions],
581
+ projectPath,
582
+ environment: this.environment,
583
+ });
584
+ // WorldState를 시스템 프롬프트에 추가
585
+ let worldStateSection = "";
586
+ if (this.worldState) {
587
+ const collector = new WorldStateCollector({ projectPath });
588
+ worldStateSection = "\n\n" + collector.formatForPrompt(this.worldState);
589
+ }
590
+ // 기존 시스템 메시지를 향상된 프롬프트로 교체
591
+ this.contextManager.replaceSystemMessage(enhancedPrompt + worldStateSection);
725
592
  // MemoryManager의 관련 학습/경고를 추가 컨텍스트로 주입
726
593
  if (this.memoryManager) {
727
594
  const memory = this.memoryManager.getMemory();
@@ -736,7 +603,6 @@ export class AgentLoop extends EventEmitter {
736
603
  }
737
604
  }
738
605
  // SkillLearner 초기화 (경험에서 학습된 스킬 자동 로드)
739
- debugLog("[slowInit] skillLearner init start");
740
606
  if (this.enableSkillLearning && projectPath) {
741
607
  try {
742
608
  this.skillLearner = new SkillLearner(projectPath);
@@ -746,10 +612,67 @@ export class AgentLoop extends EventEmitter {
746
612
  this.skillLearner = null;
747
613
  }
748
614
  }
749
- debugLog("[slowInit] skillLearner init done");
615
+ // Phase 4: FailureSignatureMemory, PlaybookLibrary, ProjectExecutive, StallDetector
616
+ if (projectPath) {
617
+ try {
618
+ this.failureSigMemory = new FailureSignatureMemory({ projectPath });
619
+ this.playbookLibrary = new PlaybookLibrary();
620
+ this.projectExecutive = new ProjectExecutive(projectPath);
621
+ // Estimated iterations from config or default 20
622
+ const estimatedIter = this.config.loop.maxIterations ?? 20;
623
+ this.stallDetector = new StallDetector(estimatedIter);
624
+ // Forward project executive events
625
+ this.projectExecutive.on("event", (ev) => this.emitEvent(ev));
626
+ // Phase 4 remaining: SelfImprovementLoop, MetaLearningCollector, TrustEconomics
627
+ this.selfImprovementLoop = new SelfImprovementLoop({ projectPath });
628
+ this.metaLearningCollector = new MetaLearningCollector({ projectPath });
629
+ this.trustEconomics = new TrustEconomics({ projectPath });
630
+ }
631
+ catch {
632
+ // Phase 4 init failure is non-fatal
633
+ }
634
+ }
635
+ // Phase 5: StrategyLearner + SkillRegistry
636
+ try {
637
+ this.strategyLearner = new StrategyLearner();
638
+ this.skillRegistry = new SkillRegistry();
639
+ // Forward Phase 5 events
640
+ this.strategyLearner.on("event", (ev) => this.emitEvent(ev));
641
+ this.skillRegistry.on("event", (ev) => this.emitEvent(ev));
642
+ }
643
+ catch {
644
+ // Phase 5 init failure is non-fatal
645
+ }
646
+ // Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
647
+ try {
648
+ this.tracePatternExtractor = new TracePatternExtractor();
649
+ this.metaLearningEngine = new MetaLearningEngine({
650
+ collector: this.metaLearningCollector ?? undefined,
651
+ strategyLearner: this.strategyLearner ?? undefined,
652
+ });
653
+ this.toolSynthesizer = new ToolSynthesizer();
654
+ // Forward Phase 5 extended events
655
+ this.tracePatternExtractor.on("event", (ev) => this.emitEvent(ev));
656
+ this.metaLearningEngine.on("event", (ev) => this.emitEvent(ev));
657
+ this.toolSynthesizer.on("event", (ev) => this.emitEvent(ev));
658
+ }
659
+ catch {
660
+ // Phase 5 extended init failure is non-fatal
661
+ }
662
+ // Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
663
+ try {
664
+ this.budgetGovernorV2 = new BudgetGovernorV2();
665
+ this.capabilityGraph = new CapabilityGraph();
666
+ this.capabilitySelfModel = new CapabilitySelfModel();
667
+ this.strategyMarket = new StrategyMarket();
668
+ this.budgetGovernorV2.on("event", (ev) => this.emitEvent(ev));
669
+ this.capabilityGraph.on("event", (ev) => this.emitEvent(ev));
670
+ this.capabilitySelfModel.on("event", (ev) => this.emitEvent(ev));
671
+ this.strategyMarket.on("event", (ev) => this.emitEvent(ev));
672
+ }
673
+ catch { /* non-fatal */ }
750
674
  // HierarchicalPlanner 생성
751
675
  this.planner = new HierarchicalPlanner({ projectPath });
752
- debugLog("[slowInit] planner created");
753
676
  if (this.skillLearner) {
754
677
  this.planner.setSkillLearner(this.skillLearner);
755
678
  }
@@ -899,44 +822,6 @@ export class AgentLoop extends EventEmitter {
899
822
  this.continuousReflection.on("reflection:context_overflow", () => {
900
823
  void this.handleSoftContextOverflow();
901
824
  });
902
- // Enrich system prompt now that we have full context (projectStructure, yuanMd, MCP tools, worldState)
903
- const enrichedPrompt = buildSystemPrompt({
904
- projectStructure: this._slowInitContext.projectStructure,
905
- yuanMdContent: this._slowInitContext.yuanMdContent,
906
- tools: [...this.config.loop.tools, ...this.mcpToolDefinitions],
907
- projectPath,
908
- environment: this.environment,
909
- model: this.config.byok.model,
910
- // NOTE: currentTaskType is undefined on turn 1 (slowInit fires before classification).
911
- // The task-type hint still reaches the LLM on turn 1 via the separate system message injection.
912
- // From turn 2+ this field carries the previous turn's classification.
913
- currentTaskType: this.currentTaskType,
914
- experienceHints: this.buildExperienceHints(),
915
- activeStrategies: this.activeStrategies.length > 0 ? this.activeStrategies : undefined,
916
- });
917
- let worldStateSection = "";
918
- if (this.worldState) {
919
- const collector = new WorldStateCollector({ projectPath, maxRecentCommits: 10, skipTest: true });
920
- worldStateSection = "\n\n" + collector.formatForPrompt(this.worldState);
921
- }
922
- this.contextManager.replaceSystemMessage(enrichedPrompt + worldStateSection);
923
- debugLog("[slowInit] DONE — context enriched for next turn");
924
- }
925
- /**
926
- * MemoryManager.learnings에서 경험 힌트를 추출한다.
927
- * confidence >= 0.4인 학습만, 최대 8개, "[category] content" 형식으로 반환.
928
- */
929
- buildExperienceHints() {
930
- if (!this.memoryManager)
931
- return [];
932
- const memory = this.memoryManager.getMemory();
933
- if (!memory?.learnings?.length)
934
- return [];
935
- return memory.learnings
936
- .filter((l) => l.confidence >= 0.4)
937
- .sort((a, b) => b.confidence - a.confidence)
938
- .slice(0, 8)
939
- .map((l) => `[${l.category}] ${l.content}`);
940
825
  }
941
826
  /**
942
827
  * MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
@@ -1005,35 +890,69 @@ export class AgentLoop extends EventEmitter {
1005
890
  this.costOptimizer.reset();
1006
891
  this.tokenBudgetManager.reset();
1007
892
  const runStartTime = Date.now();
1008
- // fastInit: < 50ms sets up ContextBudgetManager + basic system prompt
1009
- debugLog("[run] fastInit start");
1010
- await this.fastInit();
1011
- debugLog("[run] fastInit done");
1012
- // GitIntelligence: check availability + inject git context once per run
1013
- await this.initGitAvailability();
1014
- if (this.gitAvailable && this.gitIntelligence) {
893
+ // 실행 메모리/프로젝트 컨텍스트 자동 로드
894
+ await this.init();
895
+ if (!this.sessionId) {
896
+ this.sessionId = randomUUID();
897
+ }
898
+ // Phase 6: start task budget tracking
899
+ try {
900
+ this.budgetGovernorV2?.startTask(this.sessionId ?? "default");
901
+ }
902
+ catch { /* non-fatal */ }
903
+ // Initialize trace recorder (once per run)
904
+ if (!this.traceRecorder) {
905
+ this.traceRecorder = new TraceRecorder(this.sessionId);
906
+ }
907
+ // Initialize arch summarizer (once per run)
908
+ if (!this.archSummarizer && this.config.loop.projectPath) {
909
+ this.archSummarizer = new ArchSummarizer(this.config.loop.projectPath);
910
+ // Trigger refresh in background if dirty
911
+ this.archSummarizer.getSummary().catch(() => { });
912
+ }
913
+ // Phase 4: PlaybookLibrary — inject hint into system context based on goal
914
+ if (this.playbookLibrary) {
1015
915
  try {
1016
- const gitStatus = await this.gitIntelligence.getStatus();
1017
- const gitCtx = `[Git] branch=${gitStatus.currentBranch} staged=${gitStatus.stagedChanges} uncommitted=${gitStatus.uncommittedChanges} ahead=${gitStatus.aheadOfRemote}`;
1018
- this.contextManager.addMessage({ role: "system", content: gitCtx });
1019
- // Pre-fetch conflict predictions (cached for this run)
1020
- this.conflictCache = await this.gitIntelligence.predictConflicts().catch(() => []);
1021
- }
1022
- catch {
1023
- // git ops are best-effort — never block the agent
916
+ const playbook = this.playbookLibrary.query(userMessage);
917
+ if (playbook) {
918
+ this.contextManager.addMessage({
919
+ role: "system",
920
+ content: `[Playbook: ${playbook.taskType} v${playbook.version}] ` +
921
+ `Phase order: ${playbook.phaseOrder.join(" → ")}. ` +
922
+ `Stop conditions: ${playbook.stopConditions.join(", ")}. ` +
923
+ `Evidence required: ${playbook.evidenceRequirements.join(", ")}.`,
924
+ });
925
+ }
1024
926
  }
927
+ catch { /* non-fatal */ }
1025
928
  }
1026
- // slowInit: background fire-and-forget — enriches context for next turn
1027
- if (!this.slowInitPromise) {
1028
- this.slowInitPromise = this.slowInit().catch((e) => {
1029
- debugLog(`[slowInit] unhandled error: ${String(e)}`);
1030
- this.emitEvent({
1031
- kind: "agent:thinking",
1032
- content: `Background init failed — running with basic context: ${String(e)}`,
1033
- });
929
+ if (this.sessionPersistence) {
930
+ if (!this.sessionId) {
931
+ this.sessionId = randomUUID();
932
+ }
933
+ const nowIso = new Date().toISOString();
934
+ const snapshot = {
935
+ id: this.sessionId,
936
+ createdAt: nowIso,
937
+ updatedAt: nowIso,
938
+ workDir: this.config.loop.projectPath ?? "",
939
+ provider: String(this.config.byok.provider ?? "unknown"),
940
+ model: String(this.config.byok.model ?? "unknown"),
941
+ status: "running",
942
+ iteration: this.iterationCount,
943
+ tokenUsage: {
944
+ input: this.tokenUsage.input,
945
+ output: this.tokenUsage.output,
946
+ },
947
+ messageCount: this.contextManager.getMessages().length,
948
+ };
949
+ await this.sessionPersistence.save(this.sessionId, {
950
+ snapshot,
951
+ messages: this.contextManager.getMessages(),
952
+ plan: this.activePlan,
953
+ changedFiles: prevChangedFiles,
1034
954
  });
1035
955
  }
1036
- debugLog("[run] starting executeLoop");
1037
956
  // 사용자 입력 검증 (prompt injection 방어)
1038
957
  const inputValidation = this.promptDefense.validateUserInput(userMessage);
1039
958
  if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
@@ -1121,12 +1040,6 @@ export class AgentLoop extends EventEmitter {
1121
1040
  }
1122
1041
  // Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
1123
1042
  const classification = this.taskClassifier.classify(userMessage);
1124
- // Store classification type for system prompt enrichment
1125
- this.currentTaskType = classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD
1126
- ? classification.type
1127
- : undefined;
1128
- // Select active strategies based on task type and execution mode
1129
- this.activeStrategies = this.strategySelector.select(this.currentTaskType, undefined);
1130
1043
  if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1131
1044
  const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
1132
1045
  this.contextManager.addMessage({
@@ -1234,6 +1147,92 @@ export class AgentLoop extends EventEmitter {
1234
1147
  }
1235
1148
  // 실행 완료 후 메모리 자동 업데이트
1236
1149
  await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
1150
+ // Phase 4: FailureSignatureMemory promote + PlaybookLibrary outcome recording
1151
+ if (result.reason === "GOAL_ACHIEVED") {
1152
+ try {
1153
+ if (this.failureSigMemory && this.repeatedErrorSignature) {
1154
+ this.failureSigMemory.promote(this.repeatedErrorSignature, "resolved", this.allToolResults.filter(r => r.success).map(r => r.name), true);
1155
+ }
1156
+ if (this.playbookLibrary) {
1157
+ const taskType = this.playbookLibrary.query(userMessage)?.taskType;
1158
+ if (taskType) {
1159
+ this.playbookLibrary.recordOutcome(taskType, true);
1160
+ }
1161
+ }
1162
+ }
1163
+ catch { /* non-fatal */ }
1164
+ }
1165
+ // Phase 4 remaining: SelfImprovementLoop + MetaLearningCollector + TrustEconomics
1166
+ try {
1167
+ const taskType = this.playbookLibrary?.query(userMessage)?.taskType ?? "unknown";
1168
+ const toolSeq = this.allToolResults.slice(-20).map((r) => r.name);
1169
+ const runSuccess = result.reason === "GOAL_ACHIEVED";
1170
+ const runDurationMs = Date.now() - runStartTime;
1171
+ if (this.selfImprovementLoop) {
1172
+ this.selfImprovementLoop.recordOutcome({
1173
+ taskType,
1174
+ strategy: "default",
1175
+ toolSequence: toolSeq,
1176
+ success: runSuccess,
1177
+ iterationsUsed: this.iterationCount,
1178
+ tokensUsed: this.tokenUsage.total,
1179
+ durationMs: runDurationMs,
1180
+ errorSignatures: this.repeatedErrorSignature ? [this.repeatedErrorSignature] : [],
1181
+ });
1182
+ this.selfImprovementLoop.generateProposals();
1183
+ }
1184
+ if (this.metaLearningCollector) {
1185
+ this.metaLearningCollector.record({
1186
+ taskType,
1187
+ governorPolicies: {},
1188
+ toolSequence: toolSeq,
1189
+ latencyMs: runDurationMs,
1190
+ tokensUsed: this.tokenUsage.total,
1191
+ success: runSuccess,
1192
+ iterationsUsed: this.iterationCount,
1193
+ });
1194
+ }
1195
+ if (this.trustEconomics) {
1196
+ for (const tr of this.allToolResults.slice(-20)) {
1197
+ const ac = toolNameToActionClass(tr.name);
1198
+ if (ac)
1199
+ this.trustEconomics.record(ac, tr.success);
1200
+ }
1201
+ }
1202
+ // Phase 5: record strategy outcome
1203
+ if (this.strategyLearner) {
1204
+ const activePlaybookId = this._activePlaybookId ?? "default";
1205
+ if (runSuccess) {
1206
+ this.strategyLearner.recordSuccess(activePlaybookId, taskType, this.iterationCount, this.tokenUsage.total);
1207
+ }
1208
+ else {
1209
+ this.strategyLearner.recordFailure(activePlaybookId, taskType, this.iterationCount, this.tokenUsage.total);
1210
+ }
1211
+ }
1212
+ // Phase 5 extended: periodic pattern extraction (every 10 runs)
1213
+ this.sessionRunCount += 1;
1214
+ if (this.sessionRunCount % 10 === 0) {
1215
+ if (this.tracePatternExtractor) {
1216
+ this.tracePatternExtractor.extract().catch(() => { });
1217
+ }
1218
+ if (this.metaLearningEngine) {
1219
+ this.metaLearningEngine.analyze().catch(() => { });
1220
+ }
1221
+ }
1222
+ // Phase 6: record outcomes in capability self model and strategy market
1223
+ try {
1224
+ const env = this._detectedEnvironment ?? "general";
1225
+ this.capabilitySelfModel?.recordOutcome(env, taskType, runSuccess);
1226
+ this.strategyMarket?.recordResult(this._activePlaybookId ?? "default", taskType, {
1227
+ success: runSuccess,
1228
+ tokenCost: this.tokenUsage?.total ?? 0,
1229
+ latencyMs: Date.now() - runStartTime,
1230
+ });
1231
+ this.budgetGovernorV2?.endTask(this.sessionId ?? "default");
1232
+ }
1233
+ catch { /* non-fatal */ }
1234
+ }
1235
+ catch { /* non-fatal */ }
1237
1236
  // SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
1238
1237
  if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
1239
1238
  try {
@@ -1289,22 +1288,6 @@ export class AgentLoop extends EventEmitter {
1289
1288
  }
1290
1289
  // BackgroundAgentManager: 정리 (stopAll은 abort 시에만)
1291
1290
  // Background agents는 세션 간 지속되므로 여기서 stop하지 않음
1292
- // GitIntelligence: post-run commit suggestion
1293
- if (this.gitAvailable && this.gitIntelligence && this.changedFiles.length > 0) {
1294
- try {
1295
- const [commitMsg, gitStatus] = await Promise.all([
1296
- this.gitIntelligence.generateCommitMessage(false),
1297
- this.gitIntelligence.getStatus(),
1298
- ]);
1299
- if (commitMsg.subject && commitMsg.subject !== "chore: no changes") {
1300
- const suggestion = this.formatGitSuggestion(commitMsg.subject, commitMsg.body, gitStatus.currentBranch, gitStatus.aheadOfRemote, gitStatus.stagedChanges, this.changedFiles);
1301
- this.emitEvent({ kind: "agent:text_delta", text: suggestion });
1302
- }
1303
- }
1304
- catch {
1305
- // commit suggestion is best-effort
1306
- }
1307
- }
1308
1291
  return result;
1309
1292
  }
1310
1293
  catch (err) {
@@ -1572,7 +1555,7 @@ export class AgentLoop extends EventEmitter {
1572
1555
  if ((complexityOrder[complexity] ?? 0) < effectiveThreshold) {
1573
1556
  return;
1574
1557
  }
1575
- this.emitEvent({ kind: "agent:thinking", content: `planning · ${complexity} complexity` });
1558
+ this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
1576
1559
  try {
1577
1560
  const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
1578
1561
  this.activePlan = plan;
@@ -1598,7 +1581,7 @@ export class AgentLoop extends EventEmitter {
1598
1581
  role: "system",
1599
1582
  content: planContext,
1600
1583
  });
1601
- this.emitEvent({ kind: "agent:thinking", content: `plan ready · ${plan.tactical.length} tasks · risk ${plan.strategic.riskAssessment.level}` });
1584
+ this.emitSubagent("planner", "done", `plan created: ${plan.tactical.length} tasks, ${plan.totalEstimatedIterations} estimated iterations, risk ${plan.strategic.riskAssessment.level}`);
1602
1585
  }
1603
1586
  catch (err) {
1604
1587
  this.emitEvent({
@@ -1756,8 +1739,29 @@ export class AgentLoop extends EventEmitter {
1756
1739
  * (ambitious short requests that keywords miss across any language).
1757
1740
  */
1758
1741
  async detectComplexity(message) {
1759
- // Heuristic-only no LLM pre-call (eliminates 10s blocking delay before every response)
1760
- return this._detectComplexityHeuristic(message);
1742
+ const heuristic = this._detectComplexityHeuristic(message);
1743
+ // Clear extremes — trust heuristic, skip LLM call cost
1744
+ if (heuristic === "trivial" || heuristic === "massive")
1745
+ return heuristic;
1746
+ // Borderline ambiguous range: ask LLM for one-word verdict
1747
+ // Short cheap call: no tools, 1-word response, ~50 tokens total
1748
+ try {
1749
+ const resp = await this.llmClient.chat([
1750
+ {
1751
+ role: "user",
1752
+ content: `Rate this software task complexity in ONE word only (trivial/simple/moderate/complex/massive). Task: "${message.slice(0, 300)}"`,
1753
+ },
1754
+ ], []);
1755
+ const word = (resp.content ?? "").trim().toLowerCase().replace(/[^a-z]/g, "");
1756
+ const valid = ["trivial", "simple", "moderate", "complex", "massive"];
1757
+ if (valid.includes(word)) {
1758
+ return word;
1759
+ }
1760
+ }
1761
+ catch {
1762
+ // LLM classification failure — fall back to heuristic
1763
+ }
1764
+ return heuristic;
1761
1765
  }
1762
1766
  /**
1763
1767
  * HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
@@ -1895,8 +1899,17 @@ export class AgentLoop extends EventEmitter {
1895
1899
  iteration++;
1896
1900
  this.iterationCount = iteration;
1897
1901
  const iterationStart = Date.now();
1902
+ // Reset per-iteration governor state
1903
+ this.verifyRanThisIteration = false;
1904
+ this.summarizeRanThisIteration = false;
1905
+ this.llmFixerRunCount = 0;
1906
+ this.iterationSystemMsgCount = 0;
1907
+ // Message pruning — cap in-memory messages to prevent OOM (GPT recommendation)
1908
+ this.pruneMessagesIfNeeded();
1909
+ // Cap allToolResults (prevents unbounded memory growth)
1910
+ this.allToolResults = cap(this.allToolResults, BOUNDS.allToolResults);
1911
+ this.allToolResultsSinceLastReplan = cap(this.allToolResultsSinceLastReplan, BOUNDS.toolResultsSinceReplan);
1898
1912
  this.emitReasoning(`iteration ${iteration}: preparing context`);
1899
- this.iterationSystemMsgCount = 0; // Reset per-iteration (prevents accumulation across iterations)
1900
1913
  // Proactive replanning check (every 10 iterations when plan is active)
1901
1914
  if (this.replanningEngine &&
1902
1915
  this.activePlan &&
@@ -1955,10 +1968,11 @@ export class AgentLoop extends EventEmitter {
1955
1968
  // Soft context rollover:
1956
1969
  // checkpoint first, then let ContextManager compact instead of aborting/throwing.
1957
1970
  const contextUsageRatio = this.contextManager.getUsageRatio();
1958
- // Task 1: ContextBudgetManager LLM summarization at 60-70% runs BEFORE ContextCompressor
1959
- // Summarizes old "medium" priority conversation turns into a compact summary message,
1960
- // freeing tokens before the heavier ContextCompressor kicks in at 70%.
1961
- if (contextUsageRatio >= 0.60 && contextUsageRatio < 0.70 && this.contextBudgetManager && !this._contextSummarizationDone) {
1971
+ // Task 1: ContextBudgetManager LLM summarization Governor gated (default SHADOW)
1972
+ const summarizeMode = this.overheadGovernor.shouldRunSummarize(this.buildTriggerContext());
1973
+ if (summarizeMode === "BLOCKING")
1974
+ this.summarizeRanThisIteration = true;
1975
+ if (contextUsageRatio >= 0.75 && this.contextBudgetManager && !this._contextSummarizationDone && summarizeMode === "BLOCKING") {
1962
1976
  this._contextSummarizationDone = true; // run at most once per agent turn
1963
1977
  // Non-blocking: fire-and-forget so the main iteration is not stalled
1964
1978
  this.contextBudgetManager.importMessages(this.contextManager.getMessages());
@@ -2112,12 +2126,29 @@ export class AgentLoop extends EventEmitter {
2112
2126
  if (response.toolCalls.length === 0) {
2113
2127
  const content = response.content ?? "";
2114
2128
  let finalSummary = content || "Task completed.";
2129
+ // Phase transition: implement → verify when LLM signals completion (no tool calls)
2130
+ if (this.taskPhase === "implement" && this.changedFiles.length > 0) {
2131
+ this.transitionPhase("verify", "LLM completion signal (no tool calls)");
2132
+ // Run cheap checks — if they fail, stay in implement and continue loop
2133
+ const cheapOk = await this.runCheapChecks();
2134
+ if (cheapOk) {
2135
+ this.transitionPhase("finalize", "cheap checks passed");
2136
+ }
2137
+ else {
2138
+ // cheap checks failed — stay in implement, let LLM fix
2139
+ this.transitionPhase("implement", "cheap check failed, continuing");
2140
+ continue;
2141
+ }
2142
+ }
2115
2143
  const finalImpactSummary = await this.buildFinalImpactSummary();
2116
2144
  if (finalImpactSummary) {
2117
2145
  finalSummary = `${finalSummary}\n\n${finalImpactSummary}`;
2118
2146
  }
2119
- // Level 2: Deep verification before declaring completion
2120
- if (this.selfReflection && this.changedFiles.length > 0) {
2147
+ // Level 2: Deep verification before declaring completion — Governor gated (default OFF)
2148
+ const deepVerifyMode = this.overheadGovernor.shouldRunDeepVerify(this.buildTriggerContext());
2149
+ if (deepVerifyMode === "BLOCKING")
2150
+ this.verifyRanThisIteration = true;
2151
+ if (this.selfReflection && this.changedFiles.length > 0 && deepVerifyMode === "BLOCKING") {
2121
2152
  try {
2122
2153
  const changedFilesMap = this.buildChangedFilesMap();
2123
2154
  const verifyFn = async (prompt) => {
@@ -2146,10 +2177,12 @@ export class AgentLoop extends EventEmitter {
2146
2177
  this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
2147
2178
  continue; // Don't return GOAL_ACHIEVED, continue the loop
2148
2179
  }
2149
- // Level 3: Multi-agent debate for complex/massive tasks
2180
+ // Level 3: Multi-agent debate Governor gated (default OFF)
2181
+ const debateMode = this.overheadGovernor.shouldRunDebate(this.buildTriggerContext());
2150
2182
  if (this.debateOrchestrator &&
2151
2183
  ["complex", "massive"].includes(this.currentComplexity) &&
2152
- deepResult.verdict !== "pass") {
2184
+ deepResult.verdict !== "pass" &&
2185
+ debateMode === "BLOCKING") {
2153
2186
  try {
2154
2187
  this.emitSubagent("reviewer", "start", `starting debate for ${this.currentComplexity} task verification`);
2155
2188
  const debateContext = [
@@ -2316,7 +2349,8 @@ export class AgentLoop extends EventEmitter {
2316
2349
  }
2317
2350
  // Task 2: QAPipeline — run "quick" (structural only) after any WRITE tool call this iteration
2318
2351
  const projectPath = this.config.loop.projectPath;
2319
- if (this.iterationWriteToolPaths.length > 0 && projectPath) {
2352
+ const qaMode = this.overheadGovernor.shouldRunQaPipeline(this.buildTriggerContext());
2353
+ if (this.iterationWriteToolPaths.length > 0 && projectPath && qaMode !== "OFF") {
2320
2354
  try {
2321
2355
  const qaPipeline = new QAPipeline({
2322
2356
  projectPath,
@@ -2330,21 +2364,21 @@ export class AgentLoop extends EventEmitter {
2330
2364
  });
2331
2365
  const qaResult = await qaPipeline.run(this.iterationWriteToolPaths);
2332
2366
  this.lastQAResult = qaResult;
2333
- // Surface QA issues as a system message so LLM sees them next iteration
2334
2367
  const failedChecks = qaResult.stages
2335
2368
  .flatMap((s) => s.checks)
2336
2369
  .filter((c) => c.status === "fail" || c.status === "warn");
2337
2370
  const qaIssues = failedChecks
2338
2371
  .slice(0, 10)
2339
2372
  .map((c) => `[${c.severity}] ${c.name}: ${c.message}`);
2340
- // Emit structured qa_result event for TUI display
2373
+ // Always emit event for TUI display (SHADOW + BLOCKING)
2341
2374
  this.emitEvent({
2342
2375
  kind: "agent:qa_result",
2343
2376
  stage: "quick",
2344
2377
  passed: failedChecks.length === 0,
2345
2378
  issues: qaIssues,
2346
2379
  });
2347
- if (failedChecks.length > 0 && this.iterationSystemMsgCount < 5) {
2380
+ // Only inject into LLM context in BLOCKING mode (SHADOW = observe only)
2381
+ if (qaMode === "BLOCKING" && failedChecks.length > 0 && this.iterationSystemMsgCount < 5) {
2348
2382
  const checkSummary = failedChecks
2349
2383
  .slice(0, 5)
2350
2384
  .map((c) => ` - [${c.severity}] ${c.name}: ${c.message}`)
@@ -2362,13 +2396,31 @@ export class AgentLoop extends EventEmitter {
2362
2396
  }
2363
2397
  // Reset per-iteration write tool tracking
2364
2398
  this.iterationWriteToolPaths = [];
2399
+ // Phase 4: StallDetector check (non-blocking observer)
2400
+ if (this.stallDetector) {
2401
+ try {
2402
+ const stallResult = this.stallDetector.check(this.iterationCount, this.changedFiles, this.repeatedErrorSignature);
2403
+ if (stallResult.stalled && stallResult.reason) {
2404
+ this.emitEvent({
2405
+ kind: "agent:task_stalled",
2406
+ taskId: this.sessionId ?? "unknown",
2407
+ stallReason: stallResult.reason,
2408
+ iterationsElapsed: stallResult.iterationsElapsed,
2409
+ estimatedIterations: this.config.loop.maxIterations ?? 20,
2410
+ timestamp: Date.now(),
2411
+ });
2412
+ }
2413
+ }
2414
+ catch { /* non-fatal */ }
2415
+ }
2365
2416
  // Task 3: Auto-run tsc --noEmit after 2+ TS files modified in this iteration
2366
2417
  // Skip if tsc was already run in the previous iteration (cooldown)
2367
2418
  const tscFilesThisIteration = [...this.iterationTsFilesModified];
2368
2419
  this.iterationTsFilesModified = []; // reset for next iteration
2369
2420
  const tscRanPrev = this.tscRanLastIteration;
2370
2421
  this.tscRanLastIteration = false; // will set to true below if we run it
2371
- if (tscFilesThisIteration.length >= 2 && projectPath && !tscRanPrev) {
2422
+ const tscMode = this.overheadGovernor.shouldRunAutoTsc(this.buildTriggerContext());
2423
+ if (tscFilesThisIteration.length >= 2 && projectPath && !tscRanPrev && tscMode !== "OFF") {
2372
2424
  try {
2373
2425
  const tscResult = await this.toolExecutor.execute({
2374
2426
  id: `auto-tsc-${Date.now()}`,
@@ -2380,27 +2432,28 @@ export class AgentLoop extends EventEmitter {
2380
2432
  }),
2381
2433
  });
2382
2434
  this.tscRanLastIteration = true;
2383
- // Inject TypeScript errors into context so LLM sees them next iteration
2384
2435
  if (tscResult.success && tscResult.output && tscResult.output.trim().length > 0) {
2385
2436
  const tscOutput = tscResult.output.trim();
2386
- // Only inject if there are actual TS errors (output is non-empty)
2387
2437
  const hasErrors = tscOutput.includes(": error TS") || tscOutput.includes("error TS");
2388
- if (hasErrors && this.iterationSystemMsgCount < 5) {
2389
- // Truncate long tsc output to avoid context bloat
2390
- const truncated = tscOutput.length > 2000
2391
- ? tscOutput.slice(0, 2000) + "\n[...tsc output truncated]"
2392
- : tscOutput;
2393
- this.contextManager.addMessage({
2394
- role: "system",
2395
- content: `[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`,
2396
- });
2397
- this.iterationSystemMsgCount++;
2438
+ if (hasErrors) {
2439
+ // Always emit thinking event (SHADOW + BLOCKING)
2398
2440
  this.emitEvent({
2399
2441
  kind: "agent:thinking",
2400
2442
  content: `Auto-TSC: TypeScript errors found after editing ${tscFilesThisIteration.join(", ")}.`,
2401
2443
  });
2444
+ // Only inject into LLM context in BLOCKING mode
2445
+ if (tscMode === "BLOCKING" && this.iterationSystemMsgCount < 5) {
2446
+ const truncated = tscOutput.length > 2000
2447
+ ? tscOutput.slice(0, 2000) + "\n[...tsc output truncated]"
2448
+ : tscOutput;
2449
+ this.contextManager.addMessage({
2450
+ role: "system",
2451
+ content: `[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`,
2452
+ });
2453
+ this.iterationSystemMsgCount++;
2454
+ }
2402
2455
  }
2403
- else if (!hasErrors) {
2456
+ else {
2404
2457
  this.emitEvent({
2405
2458
  kind: "agent:thinking",
2406
2459
  content: `Auto-TSC: No type errors after editing ${tscFilesThisIteration.length} file(s).`,
@@ -2467,8 +2520,9 @@ export class AgentLoop extends EventEmitter {
2467
2520
  // Reflection failure is non-fatal
2468
2521
  }
2469
2522
  }
2470
- // Level 1: Quick verification after every 3rd iteration
2471
- if (this.selfReflection && iteration % 3 === 0) {
2523
+ // Level 1: Quick verification Governor gated (default SHADOW = no LLM call)
2524
+ const quickVerifyMode = this.overheadGovernor.shouldRunQuickVerify(this.buildTriggerContext());
2525
+ if (this.selfReflection && iteration % 3 === 0 && quickVerifyMode === "BLOCKING") {
2472
2526
  try {
2473
2527
  this.emitSubagent("verifier", "start", "running quick verification");
2474
2528
  const changedFilesMap = this.buildChangedFilesMap();
@@ -2506,6 +2560,8 @@ export class AgentLoop extends EventEmitter {
2506
2560
  const errorSummary = errorResults
2507
2561
  .map((r) => `${r.name}: ${r.output}`)
2508
2562
  .join("\n");
2563
+ // Track repeated error signature for Governor
2564
+ this.trackErrorSignature(errorSummary);
2509
2565
  // FailureRecovery: 근본 원인 분석 + 전략 선택
2510
2566
  const rootCause = this.failureRecovery.analyzeRootCause(errorSummary, errorResults[0]?.name);
2511
2567
  const decision = this.failureRecovery.selectStrategy(rootCause, {
@@ -2550,7 +2606,8 @@ export class AgentLoop extends EventEmitter {
2550
2606
  });
2551
2607
  // SelfDebugLoop: merge debug strategy into recovery message (not separate system msg)
2552
2608
  let debugSuffix = "";
2553
- if (iteration >= 3) {
2609
+ const llmFixerMode = this.overheadGovernor.shouldRunLlmFixer(this.buildTriggerContext());
2610
+ if (iteration >= 3 && llmFixerMode === "BLOCKING") {
2554
2611
  const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
2555
2612
  if (rootCauseAnalysis.confidence >= 0.5) {
2556
2613
  const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
@@ -2568,6 +2625,7 @@ export class AgentLoop extends EventEmitter {
2568
2625
  debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
2569
2626
  // Bug 2 fix: wire a real llmFixer so selfDebugLoop.debug() can call LLM
2570
2627
  if (debugStrategy !== "escalate") {
2628
+ this.llmFixerRunCount++;
2571
2629
  this.selfDebugLoop.debug({
2572
2630
  testCommand: testCmd,
2573
2631
  errorOutput: errorSummary,
@@ -2777,7 +2835,6 @@ export class AgentLoop extends EventEmitter {
2777
2835
  model: item.model,
2778
2836
  source: item.source ?? "llm",
2779
2837
  });
2780
- this.reasoningAdapter?.onDelta(item.text);
2781
2838
  }
2782
2839
  continue;
2783
2840
  }
@@ -2851,7 +2908,6 @@ export class AgentLoop extends EventEmitter {
2851
2908
  }
2852
2909
  if (flushTimer)
2853
2910
  clearTimeout(flushTimer);
2854
- this.reasoningAdapter?.flush();
2855
2911
  return {
2856
2912
  content: content || null,
2857
2913
  toolCalls,
@@ -2944,48 +3000,32 @@ export class AgentLoop extends EventEmitter {
2944
3000
  }
2945
3001
  // MCP 도구 호출 확인
2946
3002
  if (this.mcpClient && this.isMCPTool(toolCall.name)) {
3003
+ // Emit tool_start before execution (required for trace, QA pipeline, replay)
3004
+ this.emitEvent({
3005
+ kind: "agent:tool_start",
3006
+ tool: toolCall.name,
3007
+ input: args,
3008
+ source: "mcp",
3009
+ });
2947
3010
  const mcpResult = await this.executeMCPTool(toolCall);
3011
+ // Normalize MCP result: wrap search tool output into structured JSON if applicable
3012
+ const normalizedOutput = this.normalizeMcpResult(toolCall.name, mcpResult.output);
3013
+ const finalResult = { ...mcpResult, output: normalizedOutput };
2948
3014
  this.emitEvent({
2949
3015
  kind: "agent:tool_result",
2950
3016
  tool: toolCall.name,
2951
- output: mcpResult.output.length > 200
2952
- ? mcpResult.output.slice(0, 200) + "..."
2953
- : mcpResult.output,
2954
- durationMs: mcpResult.durationMs,
3017
+ output: finalResult.output.length > 200
3018
+ ? finalResult.output.slice(0, 200) + "..."
3019
+ : finalResult.output,
3020
+ durationMs: finalResult.durationMs,
2955
3021
  });
2956
3022
  this.emitEvent({ kind: "agent:reasoning_delta", text: `tool finished: ${toolCall.name}` });
2957
- return { result: mcpResult, deferredFixPrompt: null };
3023
+ return { result: finalResult, deferredFixPrompt: null };
2958
3024
  }
2959
3025
  // 도구 실행
2960
3026
  const startTime = Date.now();
2961
3027
  const toolAbort = new AbortController();
2962
3028
  this.interruptManager.registerToolAbort(toolAbort);
2963
- // Sandbox preflight — block disallowed tool calls before execution
2964
- if (this.sandbox) {
2965
- const sandboxCheck = this.sandbox.validateToolCall(toolCall.name, args);
2966
- if (!sandboxCheck.allowed) {
2967
- this.interruptManager.clearToolAbort();
2968
- const blocked = `[SANDBOX BLOCKED] ${sandboxCheck.violations.join("; ")}`;
2969
- this.emitEvent({ kind: "agent:tool_result", tool: toolCall.name, output: blocked, durationMs: 0 });
2970
- return {
2971
- result: { tool_call_id: toolCall.id, name: toolCall.name, output: blocked, success: false, durationMs: 0 },
2972
- deferredFixPrompt: null,
2973
- };
2974
- }
2975
- }
2976
- // Git conflict prediction — warn if file has high merge-conflict risk
2977
- if (this.conflictCache && ["file_write", "file_edit"].includes(toolCall.name)) {
2978
- const writePath = String(args.path ?? args.file ?? "");
2979
- if (writePath) {
2980
- const prediction = this.conflictCache.find((c) => c.file === writePath || writePath.endsWith(c.file) || c.file.endsWith(writePath));
2981
- if (prediction?.risk === "high") {
2982
- this.emitEvent({
2983
- kind: "agent:text_delta",
2984
- text: `\n⚠ Conflict risk (${prediction.file}): ${prediction.reason}\n`,
2985
- });
2986
- }
2987
- }
2988
- }
2989
3029
  if (["file_write", "file_edit"].includes(toolCall.name)) {
2990
3030
  const candidatePath = args.path ??
2991
3031
  args.file;
@@ -3033,29 +3073,21 @@ export class AgentLoop extends EventEmitter {
3033
3073
  });
3034
3074
  this.emitReasoning(`success: ${toolCall.name}`);
3035
3075
  this.reasoningTree.add("tool", `success: ${toolCall.name}`);
3036
- // Security Scanner postflight — scan written files for vulnerabilities (opt-in)
3037
- if (this.securityScanner && result.success && ["file_write", "file_edit"].includes(toolCall.name)) {
3038
- const writtenPath = args.path ?? args.file;
3039
- if (typeof writtenPath === "string") {
3040
- this.securityScanner.scan([writtenPath]).then((scanResult) => {
3041
- // Always render table — shows ✓ when clean, ⚠ when findings exist
3042
- const hasBad = scanResult.findings.some((f) => f.severity === "critical" || f.severity === "high");
3043
- if (hasBad || scanResult.findings.length > 0) {
3044
- this.emitEvent({
3045
- kind: "agent:text_delta",
3046
- text: formatSecurityTable(scanResult.findings, writtenPath),
3047
- });
3048
- }
3049
- }).catch(() => { });
3050
- }
3051
- }
3052
3076
  if (["file_write", "file_edit"].includes(toolCall.name) && result.success) {
3077
+ // Phase transition: explore → implement on first write
3078
+ if (this.taskPhase === "explore") {
3079
+ this.transitionPhase("implement", `first write: ${toolCall.name}`);
3080
+ }
3053
3081
  const filePath = args.path ??
3054
3082
  args.file ??
3055
3083
  "unknown";
3056
3084
  const filePathStr = String(filePath);
3057
3085
  if (!this.changedFiles.includes(filePathStr)) {
3058
3086
  this.changedFiles.push(filePathStr);
3087
+ // Cap changedFiles to prevent unbounded growth on massive refactors
3088
+ if (this.changedFiles.length > BOUNDS.changedFiles) {
3089
+ this.changedFiles = this.changedFiles.slice(-BOUNDS.changedFiles);
3090
+ }
3059
3091
  }
3060
3092
  // Task 2: track write tool paths per-iteration for QA triggering
3061
3093
  if (!this.iterationWriteToolPaths.includes(filePathStr)) {
@@ -3066,6 +3098,8 @@ export class AgentLoop extends EventEmitter {
3066
3098
  this.iterationTsFilesModified.push(filePathStr);
3067
3099
  }
3068
3100
  this.emitEvent({ kind: "agent:file_change", path: filePathStr, diff: result.output });
3101
+ // Emit evidence report (async, fire-and-forget — no loop complexity added)
3102
+ this.emitEvidenceReport(filePathStr, toolCall.name).catch(() => { });
3069
3103
  // Update world state after file modification
3070
3104
  if (this.config.loop.projectPath) {
3071
3105
  const wsProjectPath = this.config.loop.projectPath;
@@ -3773,6 +3807,44 @@ export class AgentLoop extends EventEmitter {
3773
3807
  const args = this.parseToolArgs(toolCall.arguments);
3774
3808
  return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
3775
3809
  }
3810
+ /**
3811
+ * Normalize an MCP tool result for consistent downstream consumption.
3812
+ *
3813
+ * - Search tools (tool name contains "search"): if the output is unstructured text,
3814
+ * wrap it into a JSON array of `{ title, url, snippet, source }` objects.
3815
+ * Each non-empty line is treated as a snippet entry when structured data is absent.
3816
+ * - All other tools: pass through unchanged.
3817
+ */
3818
+ normalizeMcpResult(toolName, output) {
3819
+ const isSearch = /search/i.test(toolName);
3820
+ if (!isSearch)
3821
+ return output;
3822
+ // If already valid JSON, leave as-is
3823
+ try {
3824
+ JSON.parse(output);
3825
+ return output;
3826
+ }
3827
+ catch {
3828
+ // Not JSON — wrap plain-text lines into structured objects
3829
+ }
3830
+ const lines = output
3831
+ .split("\n")
3832
+ .map((l) => l.trim())
3833
+ .filter(Boolean);
3834
+ if (lines.length === 0)
3835
+ return output;
3836
+ const structured = lines.map((line) => {
3837
+ // Attempt to extract a URL from the line
3838
+ const urlMatch = line.match(/https?:\/\/\S+/);
3839
+ return {
3840
+ title: "",
3841
+ url: urlMatch ? urlMatch[0] : "",
3842
+ snippet: line,
3843
+ source: toolName,
3844
+ };
3845
+ });
3846
+ return JSON.stringify(structured, null, 2);
3847
+ }
3776
3848
  /**
3777
3849
  * ContinuousReflection overflow signal을 soft rollover로 처리한다.
3778
3850
  * 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
@@ -3802,6 +3874,7 @@ export class AgentLoop extends EventEmitter {
3802
3874
  // cleanup failure ignored
3803
3875
  }
3804
3876
  }
3877
+ this.traceRecorder?.stop();
3805
3878
  }
3806
3879
  // ─── Helpers ───
3807
3880
  /**
@@ -3820,83 +3893,10 @@ export class AgentLoop extends EventEmitter {
3820
3893
  }
3821
3894
  return map;
3822
3895
  }
3823
- /** Check git is available and cache the result. Called once per run(). */
3824
- async initGitAvailability() {
3825
- if (!this.gitIntelligence)
3826
- return;
3827
- if (this.gitAvailable)
3828
- return; // already confirmed
3829
- try {
3830
- const { execFile: ef } = await import("node:child_process");
3831
- const { promisify } = await import("node:util");
3832
- const exec = promisify(ef);
3833
- await exec("git", ["rev-parse", "--is-inside-work-tree"], {
3834
- cwd: this.config.loop.projectPath ?? process.cwd(),
3835
- });
3836
- this.gitAvailable = true;
3837
- }
3838
- catch {
3839
- this.gitAvailable = false;
3840
- this.gitIntelligence = null; // disable for this session
3841
- }
3842
- }
3843
- /** Formats post-run commit suggestion + push status as a box-drawn terminal table */
3844
- formatGitSuggestion(subject, body, branch, ahead, staged, changedFiles) {
3845
- const R = "\x1b[0m";
3846
- const BOLD = "\x1b[1m";
3847
- const DIM = "\x1b[2m";
3848
- const GRN = "\x1b[32m";
3849
- const CYN = "\x1b[36m";
3850
- const YEL = "\x1b[33m";
3851
- const W = 58;
3852
- const h = "─";
3853
- const v = "│";
3854
- const tl = "┌";
3855
- const tr = "┐";
3856
- const bl = "└";
3857
- const br = "┘";
3858
- const lm = "├";
3859
- const rm = "┤";
3860
- const div = `${lm}${h.repeat(W)}${rm}`;
3861
- const top = `${tl}${h.repeat(W)}${tr}`;
3862
- const bot = `${bl}${h.repeat(W)}${br}`;
3863
- const pad = (s) => {
3864
- const plain = s.replace(/\x1b\[[0-9;]*m/g, "");
3865
- const fill = Math.max(0, W - 2 - plain.length);
3866
- return `${v} ${s}${" ".repeat(fill)} ${v}`;
3867
- };
3868
- const aheadStr = ahead > 0 ? `${YEL}↑${ahead} ahead${R}` : `${GRN}up-to-date${R}`;
3869
- const stagedStr = staged > 0 ? `${CYN}${staged} staged${R}` : `${DIM}nothing staged${R}`;
3870
- const filesStr = changedFiles.slice(0, 3).map(f => f.split("/").pop() ?? f).join(", ")
3871
- + (changedFiles.length > 3 ? ` +${changedFiles.length - 3}` : "");
3872
- const subjectTrunc = subject.length > W - 4 ? subject.slice(0, W - 5) + "…" : subject;
3873
- const lines = [
3874
- "",
3875
- top,
3876
- pad(` ${BOLD}💾 Commit Suggestion${R}`),
3877
- div,
3878
- pad(` ${DIM}${subjectTrunc}${R}`),
3879
- ];
3880
- if (body) {
3881
- for (const line of body.split("\n").slice(0, 3)) {
3882
- lines.push(pad(` ${DIM}${line.length > W - 4 ? line.slice(0, W - 5) + "…" : line}${R}`));
3883
- }
3884
- }
3885
- lines.push(div);
3886
- lines.push(pad(` Branch ${BOLD}${branch}${R} · ${aheadStr} · ${stagedStr}`));
3887
- lines.push(pad(` Files ${DIM}${filesStr}${R}`));
3888
- lines.push(div);
3889
- lines.push(pad(` ${DIM}git add -p${R}`));
3890
- lines.push(pad(` ${GRN}git commit -m "${subjectTrunc}"${R}`));
3891
- if (ahead > 0) {
3892
- lines.push(pad(` ${YEL}git push origin ${branch}${R} ${DIM}# ${ahead} commit(s) ahead${R}`));
3893
- }
3894
- lines.push(bot);
3895
- lines.push("");
3896
- return lines.join("\n");
3897
- }
3898
3896
  emitEvent(event) {
3899
3897
  this.emit("event", event);
3898
+ // Trace recording — fire-and-forget, never blocks
3899
+ this.traceRecorder?.record(event);
3900
3900
  }
3901
3901
  emitReasoning(content) {
3902
3902
  this.reasoningTree.add("reasoning", content);
@@ -3909,6 +3909,13 @@ export class AgentLoop extends EventEmitter {
3909
3909
  emitSubagent(name, phase, content) {
3910
3910
  this.reasoningTree.add(name, `[${phase}] ${content}`);
3911
3911
  this.emitReasoning(`[${name}:${phase}] ${content}`);
3912
+ // Route subagent events to task panel via proper events
3913
+ if (phase === "start") {
3914
+ this.emitEvent({ kind: "agent:subagent_phase", taskId: name, phase: content });
3915
+ }
3916
+ else {
3917
+ this.emitEvent({ kind: "agent:subagent_done", taskId: name, success: !content.includes("failed") });
3918
+ }
3912
3919
  }
3913
3920
  handleFatalError(err) {
3914
3921
  const message = err instanceof Error ? err.message : String(err);
@@ -3922,5 +3929,221 @@ export class AgentLoop extends EventEmitter {
3922
3929
  });
3923
3930
  return { reason: "ERROR", error: message };
3924
3931
  }
3932
+ // ─── Task phase transitions ───────────────────────────────────────────────
3933
+ /**
3934
+ * Deterministic phase transition — no LLM involved.
3935
+ * Emits agent:phase_transition event for TUI trace.
3936
+ */
3937
+ transitionPhase(to, trigger) {
3938
+ if (this.taskPhase === to)
3939
+ return;
3940
+ const from = this.taskPhase;
3941
+ this.taskPhase = to;
3942
+ this.emitEvent({
3943
+ kind: "agent:phase_transition",
3944
+ from,
3945
+ to,
3946
+ iteration: this.iterationCount,
3947
+ trigger,
3948
+ });
3949
+ this.emitReasoning(`phase: ${from} → ${to} (${trigger})`);
3950
+ }
3951
+ /**
3952
+ * Cheap local checks run in verify phase — no LLM.
3953
+ * Returns true if all checks pass (safe to advance to finalize).
3954
+ */
3955
+ async runCheapChecks() {
3956
+ const projectPath = this.config.loop.projectPath;
3957
+ // 1. Diff size check — if nothing changed, skip
3958
+ if (this.changedFiles.length === 0)
3959
+ return true;
3960
+ // 2. File existence check — all changedFiles should exist
3961
+ const { existsSync } = await import("node:fs");
3962
+ for (const f of this.changedFiles) {
3963
+ const abs = projectPath ? `${projectPath}/${f}` : f;
3964
+ if (f.startsWith("/") ? !existsSync(f) : !existsSync(abs)) {
3965
+ this.emitReasoning(`cheap check: file missing: ${f}`);
3966
+ return false;
3967
+ }
3968
+ }
3969
+ // 3. Fast syntax check — tsc --noEmit --skipLibCheck only on changed TS files
3970
+ const tsFiles = this.changedFiles.filter(f => /\.[cm]?tsx?$/.test(f));
3971
+ if (tsFiles.length > 0 && projectPath) {
3972
+ try {
3973
+ const result = await this.toolExecutor.execute({
3974
+ id: `cheap-tsc-${Date.now()}`,
3975
+ name: "shell_exec",
3976
+ arguments: JSON.stringify({
3977
+ command: `npx tsc --noEmit --skipLibCheck 2>&1 | head -20 || true`,
3978
+ cwd: projectPath,
3979
+ timeout: 30000,
3980
+ }),
3981
+ });
3982
+ if (result.success && result.output) {
3983
+ const hasErrors = result.output.includes(": error TS");
3984
+ if (hasErrors) {
3985
+ this.emitReasoning(`cheap check: tsc errors found, staying in verify`);
3986
+ // Inject TS errors so LLM sees them and fixes them
3987
+ if (this.iterationSystemMsgCount < 5) {
3988
+ const truncated = result.output.length > 1000
3989
+ ? result.output.slice(0, 1000) + "\n[truncated]"
3990
+ : result.output;
3991
+ this.contextManager.addMessage({
3992
+ role: "system",
3993
+ content: `[Verify Phase] TypeScript errors found:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix before completion.`,
3994
+ });
3995
+ this.iterationSystemMsgCount++;
3996
+ }
3997
+ return false;
3998
+ }
3999
+ }
4000
+ }
4001
+ catch {
4002
+ // cheap check failure is non-fatal — allow finalize
4003
+ }
4004
+ }
4005
+ return true;
4006
+ }
4007
+ // ─── Evidence Report ───────────────────────────────────────────────────────
4008
+ /**
4009
+ * After a file write/edit, collect diff stats + syntax signal and emit
4010
+ * `agent:evidence_report`. Runs async and never blocks the main loop.
4011
+ */
4012
+ async emitEvidenceReport(filePath, tool) {
4013
+ const timestamp = Date.now();
4014
+ const projectPath = this.config.loop.projectPath;
4015
+ // 1. Diff stats via git diff --stat
4016
+ let diffStats = null;
4017
+ try {
4018
+ const diffResult = await this.toolExecutor.execute({
4019
+ id: `evidence-diff-${timestamp}`,
4020
+ name: "shell_exec",
4021
+ arguments: JSON.stringify({
4022
+ command: `git diff --numstat HEAD -- "${filePath}" 2>/dev/null || echo "0\t0\t${filePath}"`,
4023
+ cwd: projectPath ?? ".",
4024
+ timeout: 5000,
4025
+ }),
4026
+ });
4027
+ if (diffResult.success && diffResult.output) {
4028
+ const match = diffResult.output.match(/^(\d+)\s+(\d+)/m);
4029
+ if (match) {
4030
+ diffStats = { added: parseInt(match[1], 10), removed: parseInt(match[2], 10) };
4031
+ }
4032
+ }
4033
+ }
4034
+ catch { /* non-fatal */ }
4035
+ // 2. Syntax check — skipped here to avoid spawning a duplicate tsc process.
4036
+ // runCheapChecks() already runs tsc at the implement→verify boundary and
4037
+ // the result flows to the QA pipeline. Spawning tsc per-write would
4038
+ // create N parallel tsc processes for bulk refactors.
4039
+ const syntax = "skipped";
4040
+ this.emitEvent({
4041
+ kind: "agent:evidence_report",
4042
+ filePath,
4043
+ tool,
4044
+ syntax,
4045
+ diffStats,
4046
+ lintResult: "skipped",
4047
+ timestamp,
4048
+ });
4049
+ // If a dep-graph file changed, invalidate arch summary cache
4050
+ const isDepsChange = filePath.includes("package.json") || filePath.includes("tsconfig");
4051
+ if (isDepsChange && this.archSummarizer) {
4052
+ this.archSummarizer.regenerate().catch(() => { });
4053
+ }
4054
+ }
4055
+ // ─── OverheadGovernor helpers ─────────────────────────────────────────────
4056
+ /**
4057
+ * 현재 런타임 상태로 TriggerContext 빌드.
4058
+ */
4059
+ buildTriggerContext() {
4060
+ const totalBudget = this.config.loop.totalTokenBudget;
4061
+ return {
4062
+ changedFiles: this.changedFiles,
4063
+ writeCountSinceVerify: this.writeCountSinceVerify,
4064
+ failureCount: this.allToolResults.filter(r => !r.success).length,
4065
+ repeatedErrorSignature: this.repeatedErrorSignature,
4066
+ plannerConfidence: undefined,
4067
+ contextUsageRatio: totalBudget > 0 ? this.tokenUsage.total / totalBudget : 0,
4068
+ riskyWrite: this.changedFiles.some(f => f.includes("tsconfig") || f.includes("package.json") ||
4069
+ f.endsWith("index.ts") || f.endsWith("index.tsx") || f.includes(".d.ts")),
4070
+ taskPhase: this.taskPhase,
4071
+ iteration: this.iterationCount,
4072
+ verifyRanThisIteration: this.verifyRanThisIteration,
4073
+ summarizeRanThisIteration: this.summarizeRanThisIteration,
4074
+ llmFixerRunCount: this.llmFixerRunCount,
4075
+ };
4076
+ }
4077
+ /**
4078
+ * 반복 에러 시그니처 추적 — 같은 에러가 2번 이상 나오면 repeatedErrorSignature 세팅.
4079
+ */
4080
+ trackErrorSignature(errorSummary) {
4081
+ const sig = errorSummary.slice(0, 120);
4082
+ if (sig === this._lastErrorSignature) {
4083
+ this._errorSignatureCount++;
4084
+ if (this._errorSignatureCount >= 2) {
4085
+ this.repeatedErrorSignature = sig;
4086
+ }
4087
+ }
4088
+ else {
4089
+ this._lastErrorSignature = sig;
4090
+ this._errorSignatureCount = 1;
4091
+ this.repeatedErrorSignature = undefined;
4092
+ }
4093
+ }
4094
+ // ─── Message pruning — GPT recommendation ────────────────────────────────
4095
+ static MAX_MESSAGES = 40;
4096
+ static PRUNE_KEEP_RECENT = 10;
4097
+ /**
4098
+ * 메시지 배열이 MAX_MESSAGES를 초과하면 prune.
4099
+ * 구조: [system] + [task summary] + [last PRUNE_KEEP_RECENT turns]
4100
+ *
4101
+ * "turns" = user/assistant 쌍 (tool messages는 해당 assistant 메시지에 귀속)
4102
+ */
4103
+ pruneMessagesIfNeeded() {
4104
+ const msgs = this.contextManager.getMessages();
4105
+ if (msgs.length <= AgentLoop.MAX_MESSAGES)
4106
+ return;
4107
+ const system = msgs.filter(m => m.role === "system");
4108
+ const nonSystem = msgs.filter(m => m.role !== "system");
4109
+ // last N non-system messages 유지
4110
+ const keep = nonSystem.slice(-AgentLoop.PRUNE_KEEP_RECENT);
4111
+ // pruned 범위에서 task summary 추출 (user 메시지 첫 번째)
4112
+ const pruned = nonSystem.slice(0, nonSystem.length - AgentLoop.PRUNE_KEEP_RECENT);
4113
+ const userGoals = pruned
4114
+ .filter(m => m.role === "user")
4115
+ .map(m => typeof m.content === "string" ? m.content.slice(0, 200) : "")
4116
+ .filter(Boolean)
4117
+ .slice(0, 3);
4118
+ const summaryContent = userGoals.length > 0
4119
+ ? `[Context pruned — earlier goals: ${userGoals.join(" | ")}]`
4120
+ : `[Context pruned — ${pruned.length} older messages removed]`;
4121
+ const summary = [{ role: "system", content: summaryContent }];
4122
+ // contextManager 재구성 (system + summary + recent)
4123
+ this.contextManager.clear();
4124
+ for (const m of [...system, ...summary, ...keep]) {
4125
+ this.contextManager.addMessage(m);
4126
+ }
4127
+ }
4128
+ }
4129
+ // ─── Phase 4: TrustEconomics helper ─────────────────────────────────────────
4130
+ function toolNameToActionClass(name) {
4131
+ if (name === "file_read")
4132
+ return "file_read";
4133
+ if (name === "file_write")
4134
+ return "file_write";
4135
+ if (name === "file_edit")
4136
+ return "file_edit";
4137
+ if (name === "file_delete")
4138
+ return "file_delete";
4139
+ if (name === "shell_exec")
4140
+ return "shell_exec_risky"; // conservative default
4141
+ if (name === "git_read" || name === "git_log" || name === "git_blame")
4142
+ return "git_read";
4143
+ if (name === "git_commit" || name === "git_push" || name === "git_stash")
4144
+ return "git_write";
4145
+ if (name.startsWith("mcp_"))
4146
+ return "mcp_call";
4147
+ return null;
3925
4148
  }
3926
4149
  //# sourceMappingURL=agent-loop.js.map