@yuaone/core 0.9.7 → 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 (193) 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 -29
  8. package/dist/agent-loop.d.ts.map +1 -1
  9. package/dist/agent-loop.js +697 -335
  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/research-loop.d.ts +79 -0
  113. package/dist/research-loop.d.ts.map +1 -0
  114. package/dist/research-loop.js +363 -0
  115. package/dist/research-loop.js.map +1 -0
  116. package/dist/resolve-memory-path.d.ts +32 -0
  117. package/dist/resolve-memory-path.d.ts.map +1 -0
  118. package/dist/resolve-memory-path.js +97 -0
  119. package/dist/resolve-memory-path.js.map +1 -0
  120. package/dist/safe-bounds.d.ts +101 -0
  121. package/dist/safe-bounds.d.ts.map +1 -0
  122. package/dist/safe-bounds.js +140 -0
  123. package/dist/safe-bounds.js.map +1 -0
  124. package/dist/sandbox-tiers.d.ts +5 -0
  125. package/dist/sandbox-tiers.d.ts.map +1 -1
  126. package/dist/sandbox-tiers.js +14 -6
  127. package/dist/sandbox-tiers.js.map +1 -1
  128. package/dist/security.d.ts.map +1 -1
  129. package/dist/security.js +3 -0
  130. package/dist/security.js.map +1 -1
  131. package/dist/self-improvement-loop.d.ts +64 -0
  132. package/dist/self-improvement-loop.d.ts.map +1 -0
  133. package/dist/self-improvement-loop.js +156 -0
  134. package/dist/self-improvement-loop.js.map +1 -0
  135. package/dist/session-persistence.d.ts +5 -0
  136. package/dist/session-persistence.d.ts.map +1 -1
  137. package/dist/session-persistence.js +19 -3
  138. package/dist/session-persistence.js.map +1 -1
  139. package/dist/skill-loader.d.ts +16 -9
  140. package/dist/skill-loader.d.ts.map +1 -1
  141. package/dist/skill-loader.js +52 -116
  142. package/dist/skill-loader.js.map +1 -1
  143. package/dist/skill-registry.d.ts +60 -0
  144. package/dist/skill-registry.d.ts.map +1 -0
  145. package/dist/skill-registry.js +162 -0
  146. package/dist/skill-registry.js.map +1 -0
  147. package/dist/stall-detector.d.ts +56 -0
  148. package/dist/stall-detector.d.ts.map +1 -0
  149. package/dist/stall-detector.js +142 -0
  150. package/dist/stall-detector.js.map +1 -0
  151. package/dist/strategy-learner.d.ts +57 -0
  152. package/dist/strategy-learner.d.ts.map +1 -0
  153. package/dist/strategy-learner.js +160 -0
  154. package/dist/strategy-learner.js.map +1 -0
  155. package/dist/strategy-market.d.ts +73 -0
  156. package/dist/strategy-market.d.ts.map +1 -0
  157. package/dist/strategy-market.js +200 -0
  158. package/dist/strategy-market.js.map +1 -0
  159. package/dist/sub-agent.d.ts +0 -3
  160. package/dist/sub-agent.d.ts.map +1 -1
  161. package/dist/sub-agent.js +0 -10
  162. package/dist/sub-agent.js.map +1 -1
  163. package/dist/system-prompt.d.ts +0 -2
  164. package/dist/system-prompt.d.ts.map +1 -1
  165. package/dist/system-prompt.js +97 -490
  166. package/dist/system-prompt.js.map +1 -1
  167. package/dist/task-classifier.d.ts.map +1 -1
  168. package/dist/task-classifier.js +2 -54
  169. package/dist/task-classifier.js.map +1 -1
  170. package/dist/tool-synthesizer.d.ts +149 -0
  171. package/dist/tool-synthesizer.d.ts.map +1 -0
  172. package/dist/tool-synthesizer.js +455 -0
  173. package/dist/tool-synthesizer.js.map +1 -0
  174. package/dist/trace-pattern-extractor.d.ts +76 -0
  175. package/dist/trace-pattern-extractor.d.ts.map +1 -0
  176. package/dist/trace-pattern-extractor.js +321 -0
  177. package/dist/trace-pattern-extractor.js.map +1 -0
  178. package/dist/trace-recorder.d.ts +38 -0
  179. package/dist/trace-recorder.d.ts.map +1 -0
  180. package/dist/trace-recorder.js +94 -0
  181. package/dist/trace-recorder.js.map +1 -0
  182. package/dist/trust-economics.d.ts +50 -0
  183. package/dist/trust-economics.d.ts.map +1 -0
  184. package/dist/trust-economics.js +148 -0
  185. package/dist/trust-economics.js.map +1 -0
  186. package/dist/types.d.ts +272 -3
  187. package/dist/types.d.ts.map +1 -1
  188. package/dist/types.js.map +1 -1
  189. package/dist/yuan-md-loader.d.ts +22 -0
  190. package/dist/yuan-md-loader.d.ts.map +1 -0
  191. package/dist/yuan-md-loader.js +75 -0
  192. package/dist/yuan-md-loader.js.map +1 -0
  193. 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,10 +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";
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";
72
79
  /**
73
80
  * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
74
81
  *
@@ -98,87 +105,6 @@ import { SecurityScanner } from "./security-scanner.js";
98
105
  */
99
106
  /** Minimum confidence for classification-based hints/routing to activate */
100
107
  const CLASSIFICATION_CONFIDENCE_THRESHOLD = 0.6;
101
- // ─── Security Table Formatter ─────────────────────────────────────────────────
102
- /**
103
- * Renders SecurityScanner findings as a box-drawn terminal table with ANSI colors.
104
- *
105
- * ⚠ Security Scan · src/app.ts
106
- * ┌────────────┬─────────┬──────────────────────────────────┐
107
- * │ Severity │ Count │ Rules │
108
- * ├────────────┼─────────┼──────────────────────────────────┤
109
- * │ CRITICAL │ 1 │ hardcoded-secret │
110
- * │ HIGH │ 2 │ sql-injection, xss-reflected │
111
- * │ MEDIUM │ 0 │ — │
112
- * │ LOW │ 1 │ weak-hash │
113
- * └────────────┴─────────┴──────────────────────────────────┘
114
- */
115
- function formatSecurityTable(findings, filePath) {
116
- // ANSI
117
- const R = "\x1b[0m";
118
- const BOLD = "\x1b[1m";
119
- const DIM = "\x1b[2m";
120
- const RED = "\x1b[1;31m";
121
- const YEL = "\x1b[33m";
122
- const CYN = "\x1b[36m";
123
- const GRN = "\x1b[32m";
124
- const WHT = "\x1b[1;37m";
125
- // Aggregate
126
- const counts = { critical: 0, high: 0, medium: 0, low: 0 };
127
- const ruleMap = { critical: [], high: [], medium: [], low: [] };
128
- for (const f of findings) {
129
- const sev = f.severity.toLowerCase();
130
- if (sev in counts) {
131
- counts[sev]++;
132
- if (sev in ruleMap && !ruleMap[sev].includes(f.rule))
133
- ruleMap[sev].push(f.rule);
134
- }
135
- }
136
- const totalBad = counts.critical + counts.high;
137
- // Column widths (content only, borders + spaces added separately)
138
- const C1 = 10; // severity label
139
- const C2 = 7; // count
140
- const C3 = 34; // rules
141
- const h = "─";
142
- const v = "│";
143
- const tl = "┌";
144
- const tr = "┐";
145
- const bl = "└";
146
- const br = "┘";
147
- const lm = "├";
148
- const rm = "┤";
149
- const tm = "┬";
150
- const bm = "┴";
151
- const x = "┼";
152
- const divider = (L, M, Ri) => `${L}${h.repeat(C1 + 2)}${M}${h.repeat(C2 + 2)}${M}${h.repeat(C3 + 2)}${Ri}`;
153
- const row = (label, color, count, rules) => {
154
- const rulesStr = rules.length > 0 ? rules.join(", ") : "—";
155
- const col1 = label.padEnd(C1);
156
- const col2 = String(count).padStart(3).padEnd(C2);
157
- const col3 = (rulesStr.length > C3 ? rulesStr.slice(0, C3 - 1) + "…" : rulesStr).padEnd(C3);
158
- const c = count > 0 ? color : DIM;
159
- return `${v} ${c}${col1}${R} ${v} ${c}${col2}${R} ${v} ${c}${col3}${R} ${v}`;
160
- };
161
- const headerCol1 = "Severity".padEnd(C1);
162
- const headerCol2 = "Count".padEnd(C2);
163
- const headerCol3 = "Rules".padEnd(C3);
164
- const header = `${v} ${WHT}${headerCol1}${R} ${v} ${WHT}${headerCol2}${R} ${v} ${WHT}${headerCol3}${R} ${v}`;
165
- const shortPath = filePath.length > 44 ? "…" + filePath.slice(-43) : filePath;
166
- const icon = totalBad > 0 ? `${YEL}⚠${R}` : `${GRN}✓${R}`;
167
- return [
168
- "",
169
- `${icon} ${BOLD}Security Scan${R} ${DIM}· ${shortPath}${R}`,
170
- "",
171
- divider(tl, tm, tr),
172
- header,
173
- divider(lm, x, rm),
174
- row("CRITICAL", RED, counts.critical, ruleMap.critical),
175
- row("HIGH", YEL, counts.high, ruleMap.high),
176
- row("MEDIUM", CYN, counts.medium, ruleMap.medium),
177
- row("LOW", DIM, counts.low, ruleMap.low),
178
- divider(bl, bm, br),
179
- "",
180
- ].join("\n");
181
- }
182
108
  export class AgentLoop extends EventEmitter {
183
109
  abortSignal;
184
110
  llmClient;
@@ -206,10 +132,7 @@ export class AgentLoop extends EventEmitter {
206
132
  _lastInjectedTaskIndex = -1; // track when plan progress was last injected
207
133
  changedFiles = [];
208
134
  aborted = false;
209
- fastInitialized = false;
210
- slowInitDone = false;
211
- slowInitPromise = null;
212
- _slowInitContext = {};
135
+ initialized = false;
213
136
  continuationEngine = null;
214
137
  mcpClient = null;
215
138
  mcpToolDefinitions = [];
@@ -240,8 +163,6 @@ export class AgentLoop extends EventEmitter {
240
163
  skillLearner = null;
241
164
  repoGraph = null;
242
165
  backgroundAgentManager = null;
243
- sandbox = null;
244
- securityScanner = null;
245
166
  sessionPersistence = null;
246
167
  sessionId = null;
247
168
  enableToolPlanning;
@@ -249,9 +170,6 @@ export class AgentLoop extends EventEmitter {
249
170
  enableBackgroundAgents;
250
171
  currentToolPlan = null;
251
172
  executedToolNames = [];
252
- currentTaskType = undefined;
253
- strategySelector = new StrategySelector();
254
- activeStrategies = [];
255
173
  /** Context Budget: max 3 active skills at once */
256
174
  activeSkillIds = [];
257
175
  static MAX_ACTIVE_SKILLS = 3;
@@ -269,6 +187,22 @@ export class AgentLoop extends EventEmitter {
269
187
  iterationTsFilesModified = [];
270
188
  /** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
271
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;
272
206
  /** PersonaManager — learns user communication style, injects persona into system prompt */
273
207
  personaManager = null;
274
208
  /** InMemoryVectorStore — RAG: semantic code context retrieval for relevant snippets */
@@ -297,6 +231,28 @@ export class AgentLoop extends EventEmitter {
297
231
  reasoningAggregator = new ReasoningAggregator();
298
232
  reasoningTree = new ReasoningTree();
299
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;
300
256
  /**
301
257
  * Restore AgentLoop state from persisted session (yuan resume)
302
258
  */
@@ -360,7 +316,7 @@ export class AgentLoop extends EventEmitter {
360
316
  this.abortSignal = options.abortSignal;
361
317
  this.enableMemory = options.enableMemory !== false;
362
318
  this.enablePlanning = options.enablePlanning !== false;
363
- this.planningThreshold = options.planningThreshold ?? "complex";
319
+ this.planningThreshold = options.planningThreshold ?? "moderate";
364
320
  this.environment = options.environment;
365
321
  this.enableSelfReflection = options.enableSelfReflection !== false;
366
322
  this.enableDebate = options.enableDebate !== false;
@@ -368,6 +324,11 @@ export class AgentLoop extends EventEmitter {
368
324
  this.llmClient = new BYOKClient(options.config.byok);
369
325
  // Governor 생성
370
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
+ });
371
332
  // ContextManager 생성
372
333
  this.contextManager = new ContextManager({
373
334
  maxContextTokens: options.contextConfig?.maxContextTokens ??
@@ -407,18 +368,6 @@ export class AgentLoop extends EventEmitter {
407
368
  this.enableToolPlanning = options.enableToolPlanning !== false;
408
369
  this.enableSkillLearning = options.enableSkillLearning !== false;
409
370
  this.enableBackgroundAgents = options.enableBackgroundAgents === true;
410
- // SandboxManager — preflight guard (auto-instantiated from projectPath)
411
- const sandboxProject = options.sandboxConfig?.projectPath ?? options.config.loop.projectPath;
412
- if (sandboxProject) {
413
- this.sandbox = new SandboxManager({
414
- projectPath: sandboxProject,
415
- ...(options.sandboxConfig ?? {}),
416
- });
417
- }
418
- // SecurityScanner — postflight guard (opt-in)
419
- if (options.enableSecurityScan && sandboxProject) {
420
- this.securityScanner = new SecurityScanner({ projectPath: sandboxProject });
421
- }
422
371
  // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
423
372
  this.contextManager.addMessage({
424
373
  role: "system",
@@ -426,45 +375,21 @@ export class AgentLoop extends EventEmitter {
426
375
  });
427
376
  }
428
377
  /**
429
- * Fast init runs synchronously before LLM call (< 50ms).
430
- * Sets up ContextBudgetManager and a minimal system prompt.
431
- * Idempotent: subsequent calls are no-ops.
378
+ * Memory와 프로젝트 컨텍스트를 로드하여 시스템 프롬프트를 갱신.
379
+ * run() 호출 전에 호출하면 메모리가 자동으로 주입된다.
380
+ * 이미 초기화되었으면 스킵.
432
381
  */
433
- async fastInit() {
434
- if (this.fastInitialized)
382
+ async init() {
383
+ if (this.initialized)
435
384
  return;
436
- this.fastInitialized = true;
437
- // ContextBudgetManager must be ready before any LLM call
385
+ this.initialized = true;
386
+ // Task 1: Initialize ContextBudgetManager with the total token budget
438
387
  this.contextBudgetManager = new ContextBudgetManager({
439
388
  totalBudget: this.config.loop.totalTokenBudget,
440
389
  enableSummarization: true,
441
- summarizationThreshold: 0.60,
390
+ summarizationThreshold: 0.60, // trigger summarize() at 60% (before ContextCompressor at 70%)
442
391
  });
443
392
  const projectPath = this.config.loop.projectPath;
444
- if (!projectPath)
445
- return;
446
- // Minimal system prompt — no project structure yet (slowInit provides it later)
447
- const basicPrompt = buildSystemPrompt({
448
- projectStructure: undefined,
449
- yuanMdContent: undefined,
450
- tools: [...this.config.loop.tools],
451
- projectPath,
452
- environment: this.environment,
453
- model: this.config.byok.model,
454
- });
455
- this.contextManager.replaceSystemMessage(basicPrompt);
456
- debugLog("[fastInit] done — LLM ready");
457
- }
458
- /**
459
- * Slow init — background fire-and-forget.
460
- * Loads memory, world state, MCP, etc. and enriches system prompt for next turn.
461
- * Idempotent: subsequent calls are no-ops.
462
- */
463
- async slowInit() {
464
- if (this.slowInitDone)
465
- return;
466
- this.slowInitDone = true;
467
- const projectPath = this.config.loop.projectPath;
468
393
  if (!projectPath)
469
394
  return;
470
395
  // Session persistence init
@@ -474,47 +399,12 @@ export class AgentLoop extends EventEmitter {
474
399
  catch {
475
400
  this.sessionPersistence = null;
476
401
  }
477
- // Session save (moved from run() into slowInit)
478
- if (this.sessionPersistence) {
479
- try {
480
- if (!this.sessionId) {
481
- this.sessionId = randomUUID();
482
- }
483
- const nowIso = new Date().toISOString();
484
- const snapshot = {
485
- id: this.sessionId,
486
- createdAt: nowIso,
487
- updatedAt: nowIso,
488
- workDir: projectPath,
489
- provider: String(this.config.byok.provider ?? "unknown"),
490
- model: String(this.config.byok.model ?? "unknown"),
491
- status: "running",
492
- iteration: this.iterationCount,
493
- tokenUsage: {
494
- input: this.tokenUsage.input,
495
- output: this.tokenUsage.output,
496
- },
497
- messageCount: this.contextManager.getMessages().length,
498
- };
499
- await this.sessionPersistence.save(this.sessionId, {
500
- snapshot,
501
- messages: this.contextManager.getMessages(),
502
- plan: this.activePlan,
503
- changedFiles: [],
504
- });
505
- }
506
- catch {
507
- // Session save failure is non-fatal
508
- }
509
- }
510
402
  let yuanMdContent;
511
403
  let projectStructure;
512
- debugLog("[slowInit] start");
513
404
  // Memory 로드
514
405
  if (this.enableMemory) {
515
406
  try {
516
407
  // YUAN.md (raw markdown)
517
- debugLog("[slowInit] yuanMemory.load start");
518
408
  this.yuanMemory = new YuanMemory(projectPath);
519
409
  const memData = await this.yuanMemory.load();
520
410
  if (memData) {
@@ -544,20 +434,8 @@ export class AgentLoop extends EventEmitter {
544
434
  const indexer = new CodeIndexer({});
545
435
  indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
546
436
  }).catch(() => { });
547
- // 프로젝트 구조 분석 (5초 타임아웃)
548
- debugLog("[slowInit] analyzeProject start");
549
- const analyzeTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("analyzeProject timeout")), 15000));
550
- projectStructure = await Promise.race([
551
- this.yuanMemory.analyzeProject(),
552
- analyzeTimeout,
553
- ]).catch((e) => {
554
- debugLog(`[slowInit] analyzeProject skipped: ${String(e)}`);
555
- return undefined;
556
- });
557
- debugLog("[slowInit] analyzeProject done");
558
- // Store for enriched system prompt at end of slowInit
559
- this._slowInitContext.yuanMdContent = yuanMdContent;
560
- this._slowInitContext.projectStructure = projectStructure;
437
+ // 프로젝트 구조 분석
438
+ projectStructure = await this.yuanMemory.analyzeProject();
561
439
  }
562
440
  catch (memErr) {
563
441
  // 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
@@ -584,7 +462,6 @@ export class AgentLoop extends EventEmitter {
584
462
  catch {
585
463
  // 정책 로드 실패 → 기본값 사용
586
464
  }
587
- debugLog("[slowInit] worldState.collect start");
588
465
  // WorldState 수집 → system prompt에 주입
589
466
  try {
590
467
  const worldStateCollector = new WorldStateCollector({
@@ -593,11 +470,9 @@ export class AgentLoop extends EventEmitter {
593
470
  skipTest: true,
594
471
  });
595
472
  this.worldState = await worldStateCollector.collect();
596
- this._slowInitContext.worldState = this.worldState ?? undefined;
597
- debugLog("[slowInit] worldState.collect done");
598
473
  }
599
- catch (e) {
600
- debugLog(`[slowInit] worldState.collect ERROR: ${String(e)}`);
474
+ catch {
475
+ // WorldState 수집 실패는 치명적이지 않음
601
476
  }
602
477
  // Initialize World Model
603
478
  if (this.worldState && projectPath) {
@@ -625,13 +500,11 @@ export class AgentLoop extends EventEmitter {
625
500
  catch {
626
501
  // Not a git repo or git unavailable — FailureRecovery will use file-level rollback only
627
502
  }
628
- debugLog("[slowInit] continuationEngine + MCP start");
629
503
  // ImpactAnalyzer 생성
630
504
  this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
631
505
  // ContinuationEngine 생성
632
506
  this.continuationEngine = new ContinuationEngine({ projectPath });
633
507
  // 이전 세션 체크포인트 복원
634
- debugLog("[slowInit] checkpoint restore start");
635
508
  try {
636
509
  const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
637
510
  if (latestCheckpoint) {
@@ -647,9 +520,7 @@ export class AgentLoop extends EventEmitter {
647
520
  catch {
648
521
  // 체크포인트 복원 실패는 치명적이지 않음
649
522
  }
650
- debugLog("[slowInit] checkpoint restore done");
651
523
  // MCP 클라이언트 연결
652
- debugLog("[slowInit] MCP connect start");
653
524
  if (this.mcpServerConfigs.length > 0) {
654
525
  try {
655
526
  this.mcpClient = new MCPClient({
@@ -664,7 +535,6 @@ export class AgentLoop extends EventEmitter {
664
535
  this.mcpToolDefinitions = [];
665
536
  }
666
537
  }
667
- debugLog("[slowInit] MCP connect done");
668
538
  // ReflexionEngine 생성
669
539
  if (projectPath) {
670
540
  this.reflexionEngine = new ReflexionEngine({ projectPath });
@@ -703,6 +573,22 @@ export class AgentLoop extends EventEmitter {
703
573
  totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
704
574
  });
705
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);
706
592
  // MemoryManager의 관련 학습/경고를 추가 컨텍스트로 주입
707
593
  if (this.memoryManager) {
708
594
  const memory = this.memoryManager.getMemory();
@@ -717,7 +603,6 @@ export class AgentLoop extends EventEmitter {
717
603
  }
718
604
  }
719
605
  // SkillLearner 초기화 (경험에서 학습된 스킬 자동 로드)
720
- debugLog("[slowInit] skillLearner init start");
721
606
  if (this.enableSkillLearning && projectPath) {
722
607
  try {
723
608
  this.skillLearner = new SkillLearner(projectPath);
@@ -727,10 +612,67 @@ export class AgentLoop extends EventEmitter {
727
612
  this.skillLearner = null;
728
613
  }
729
614
  }
730
- 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 */ }
731
674
  // HierarchicalPlanner 생성
732
675
  this.planner = new HierarchicalPlanner({ projectPath });
733
- debugLog("[slowInit] planner created");
734
676
  if (this.skillLearner) {
735
677
  this.planner.setSkillLearner(this.skillLearner);
736
678
  }
@@ -880,44 +822,6 @@ export class AgentLoop extends EventEmitter {
880
822
  this.continuousReflection.on("reflection:context_overflow", () => {
881
823
  void this.handleSoftContextOverflow();
882
824
  });
883
- // Enrich system prompt now that we have full context (projectStructure, yuanMd, MCP tools, worldState)
884
- const enrichedPrompt = buildSystemPrompt({
885
- projectStructure: this._slowInitContext.projectStructure,
886
- yuanMdContent: this._slowInitContext.yuanMdContent,
887
- tools: [...this.config.loop.tools, ...this.mcpToolDefinitions],
888
- projectPath,
889
- environment: this.environment,
890
- model: this.config.byok.model,
891
- // NOTE: currentTaskType is undefined on turn 1 (slowInit fires before classification).
892
- // The task-type hint still reaches the LLM on turn 1 via the separate system message injection.
893
- // From turn 2+ this field carries the previous turn's classification.
894
- currentTaskType: this.currentTaskType,
895
- experienceHints: this.buildExperienceHints(),
896
- activeStrategies: this.activeStrategies.length > 0 ? this.activeStrategies : undefined,
897
- });
898
- let worldStateSection = "";
899
- if (this.worldState) {
900
- const collector = new WorldStateCollector({ projectPath, maxRecentCommits: 10, skipTest: true });
901
- worldStateSection = "\n\n" + collector.formatForPrompt(this.worldState);
902
- }
903
- this.contextManager.replaceSystemMessage(enrichedPrompt + worldStateSection);
904
- debugLog("[slowInit] DONE — context enriched for next turn");
905
- }
906
- /**
907
- * MemoryManager.learnings에서 경험 힌트를 추출한다.
908
- * confidence >= 0.4인 학습만, 최대 8개, "[category] content" 형식으로 반환.
909
- */
910
- buildExperienceHints() {
911
- if (!this.memoryManager)
912
- return [];
913
- const memory = this.memoryManager.getMemory();
914
- if (!memory?.learnings?.length)
915
- return [];
916
- return memory.learnings
917
- .filter((l) => l.confidence >= 0.4)
918
- .sort((a, b) => b.confidence - a.confidence)
919
- .slice(0, 8)
920
- .map((l) => `[${l.category}] ${l.content}`);
921
825
  }
922
826
  /**
923
827
  * MemoryManager의 학습/실패 기록을 시스템 메시지로 변환.
@@ -986,21 +890,69 @@ export class AgentLoop extends EventEmitter {
986
890
  this.costOptimizer.reset();
987
891
  this.tokenBudgetManager.reset();
988
892
  const runStartTime = Date.now();
989
- // fastInit: < 50ms sets up ContextBudgetManager + basic system prompt
990
- debugLog("[run] fastInit start");
991
- await this.fastInit();
992
- debugLog("[run] fastInit done");
993
- // slowInit: background fire-and-forget — enriches context for next turn
994
- if (!this.slowInitPromise) {
995
- this.slowInitPromise = this.slowInit().catch((e) => {
996
- debugLog(`[slowInit] unhandled error: ${String(e)}`);
997
- this.emitEvent({
998
- kind: "agent:thinking",
999
- content: `Background init failed running with basic context: ${String(e)}`,
1000
- });
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) {
915
+ try {
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
+ }
926
+ }
927
+ catch { /* non-fatal */ }
928
+ }
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,
1001
954
  });
1002
955
  }
1003
- debugLog("[run] starting executeLoop");
1004
956
  // 사용자 입력 검증 (prompt injection 방어)
1005
957
  const inputValidation = this.promptDefense.validateUserInput(userMessage);
1006
958
  if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
@@ -1088,12 +1040,6 @@ export class AgentLoop extends EventEmitter {
1088
1040
  }
1089
1041
  // Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
1090
1042
  const classification = this.taskClassifier.classify(userMessage);
1091
- // Store classification type for system prompt enrichment
1092
- this.currentTaskType = classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD
1093
- ? classification.type
1094
- : undefined;
1095
- // Select active strategies based on task type and execution mode
1096
- this.activeStrategies = this.strategySelector.select(this.currentTaskType, undefined);
1097
1043
  if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1098
1044
  const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
1099
1045
  this.contextManager.addMessage({
@@ -1201,6 +1147,92 @@ export class AgentLoop extends EventEmitter {
1201
1147
  }
1202
1148
  // 실행 완료 후 메모리 자동 업데이트
1203
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 */ }
1204
1236
  // SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
1205
1237
  if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
1206
1238
  try {
@@ -1523,7 +1555,7 @@ export class AgentLoop extends EventEmitter {
1523
1555
  if ((complexityOrder[complexity] ?? 0) < effectiveThreshold) {
1524
1556
  return;
1525
1557
  }
1526
- this.emitEvent({ kind: "agent:thinking", content: `planning · ${complexity} complexity` });
1558
+ this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
1527
1559
  try {
1528
1560
  const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
1529
1561
  this.activePlan = plan;
@@ -1549,7 +1581,7 @@ export class AgentLoop extends EventEmitter {
1549
1581
  role: "system",
1550
1582
  content: planContext,
1551
1583
  });
1552
- 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}`);
1553
1585
  }
1554
1586
  catch (err) {
1555
1587
  this.emitEvent({
@@ -1707,8 +1739,29 @@ export class AgentLoop extends EventEmitter {
1707
1739
  * (ambitious short requests that keywords miss across any language).
1708
1740
  */
1709
1741
  async detectComplexity(message) {
1710
- // Heuristic-only no LLM pre-call (eliminates 10s blocking delay before every response)
1711
- 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;
1712
1765
  }
1713
1766
  /**
1714
1767
  * HierarchicalPlan을 LLM이 따라갈 수 있는 컨텍스트 메시지로 포맷.
@@ -1846,8 +1899,17 @@ export class AgentLoop extends EventEmitter {
1846
1899
  iteration++;
1847
1900
  this.iterationCount = iteration;
1848
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);
1849
1912
  this.emitReasoning(`iteration ${iteration}: preparing context`);
1850
- this.iterationSystemMsgCount = 0; // Reset per-iteration (prevents accumulation across iterations)
1851
1913
  // Proactive replanning check (every 10 iterations when plan is active)
1852
1914
  if (this.replanningEngine &&
1853
1915
  this.activePlan &&
@@ -1906,10 +1968,11 @@ export class AgentLoop extends EventEmitter {
1906
1968
  // Soft context rollover:
1907
1969
  // checkpoint first, then let ContextManager compact instead of aborting/throwing.
1908
1970
  const contextUsageRatio = this.contextManager.getUsageRatio();
1909
- // Task 1: ContextBudgetManager LLM summarization at 60-70% runs BEFORE ContextCompressor
1910
- // Summarizes old "medium" priority conversation turns into a compact summary message,
1911
- // freeing tokens before the heavier ContextCompressor kicks in at 70%.
1912
- 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") {
1913
1976
  this._contextSummarizationDone = true; // run at most once per agent turn
1914
1977
  // Non-blocking: fire-and-forget so the main iteration is not stalled
1915
1978
  this.contextBudgetManager.importMessages(this.contextManager.getMessages());
@@ -2063,12 +2126,29 @@ export class AgentLoop extends EventEmitter {
2063
2126
  if (response.toolCalls.length === 0) {
2064
2127
  const content = response.content ?? "";
2065
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
+ }
2066
2143
  const finalImpactSummary = await this.buildFinalImpactSummary();
2067
2144
  if (finalImpactSummary) {
2068
2145
  finalSummary = `${finalSummary}\n\n${finalImpactSummary}`;
2069
2146
  }
2070
- // Level 2: Deep verification before declaring completion
2071
- 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") {
2072
2152
  try {
2073
2153
  const changedFilesMap = this.buildChangedFilesMap();
2074
2154
  const verifyFn = async (prompt) => {
@@ -2097,10 +2177,12 @@ export class AgentLoop extends EventEmitter {
2097
2177
  this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
2098
2178
  continue; // Don't return GOAL_ACHIEVED, continue the loop
2099
2179
  }
2100
- // 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());
2101
2182
  if (this.debateOrchestrator &&
2102
2183
  ["complex", "massive"].includes(this.currentComplexity) &&
2103
- deepResult.verdict !== "pass") {
2184
+ deepResult.verdict !== "pass" &&
2185
+ debateMode === "BLOCKING") {
2104
2186
  try {
2105
2187
  this.emitSubagent("reviewer", "start", `starting debate for ${this.currentComplexity} task verification`);
2106
2188
  const debateContext = [
@@ -2267,7 +2349,8 @@ export class AgentLoop extends EventEmitter {
2267
2349
  }
2268
2350
  // Task 2: QAPipeline — run "quick" (structural only) after any WRITE tool call this iteration
2269
2351
  const projectPath = this.config.loop.projectPath;
2270
- if (this.iterationWriteToolPaths.length > 0 && projectPath) {
2352
+ const qaMode = this.overheadGovernor.shouldRunQaPipeline(this.buildTriggerContext());
2353
+ if (this.iterationWriteToolPaths.length > 0 && projectPath && qaMode !== "OFF") {
2271
2354
  try {
2272
2355
  const qaPipeline = new QAPipeline({
2273
2356
  projectPath,
@@ -2281,21 +2364,21 @@ export class AgentLoop extends EventEmitter {
2281
2364
  });
2282
2365
  const qaResult = await qaPipeline.run(this.iterationWriteToolPaths);
2283
2366
  this.lastQAResult = qaResult;
2284
- // Surface QA issues as a system message so LLM sees them next iteration
2285
2367
  const failedChecks = qaResult.stages
2286
2368
  .flatMap((s) => s.checks)
2287
2369
  .filter((c) => c.status === "fail" || c.status === "warn");
2288
2370
  const qaIssues = failedChecks
2289
2371
  .slice(0, 10)
2290
2372
  .map((c) => `[${c.severity}] ${c.name}: ${c.message}`);
2291
- // Emit structured qa_result event for TUI display
2373
+ // Always emit event for TUI display (SHADOW + BLOCKING)
2292
2374
  this.emitEvent({
2293
2375
  kind: "agent:qa_result",
2294
2376
  stage: "quick",
2295
2377
  passed: failedChecks.length === 0,
2296
2378
  issues: qaIssues,
2297
2379
  });
2298
- 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) {
2299
2382
  const checkSummary = failedChecks
2300
2383
  .slice(0, 5)
2301
2384
  .map((c) => ` - [${c.severity}] ${c.name}: ${c.message}`)
@@ -2313,13 +2396,31 @@ export class AgentLoop extends EventEmitter {
2313
2396
  }
2314
2397
  // Reset per-iteration write tool tracking
2315
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
+ }
2316
2416
  // Task 3: Auto-run tsc --noEmit after 2+ TS files modified in this iteration
2317
2417
  // Skip if tsc was already run in the previous iteration (cooldown)
2318
2418
  const tscFilesThisIteration = [...this.iterationTsFilesModified];
2319
2419
  this.iterationTsFilesModified = []; // reset for next iteration
2320
2420
  const tscRanPrev = this.tscRanLastIteration;
2321
2421
  this.tscRanLastIteration = false; // will set to true below if we run it
2322
- if (tscFilesThisIteration.length >= 2 && projectPath && !tscRanPrev) {
2422
+ const tscMode = this.overheadGovernor.shouldRunAutoTsc(this.buildTriggerContext());
2423
+ if (tscFilesThisIteration.length >= 2 && projectPath && !tscRanPrev && tscMode !== "OFF") {
2323
2424
  try {
2324
2425
  const tscResult = await this.toolExecutor.execute({
2325
2426
  id: `auto-tsc-${Date.now()}`,
@@ -2331,27 +2432,28 @@ export class AgentLoop extends EventEmitter {
2331
2432
  }),
2332
2433
  });
2333
2434
  this.tscRanLastIteration = true;
2334
- // Inject TypeScript errors into context so LLM sees them next iteration
2335
2435
  if (tscResult.success && tscResult.output && tscResult.output.trim().length > 0) {
2336
2436
  const tscOutput = tscResult.output.trim();
2337
- // Only inject if there are actual TS errors (output is non-empty)
2338
2437
  const hasErrors = tscOutput.includes(": error TS") || tscOutput.includes("error TS");
2339
- if (hasErrors && this.iterationSystemMsgCount < 5) {
2340
- // Truncate long tsc output to avoid context bloat
2341
- const truncated = tscOutput.length > 2000
2342
- ? tscOutput.slice(0, 2000) + "\n[...tsc output truncated]"
2343
- : tscOutput;
2344
- this.contextManager.addMessage({
2345
- role: "system",
2346
- content: `[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`,
2347
- });
2348
- this.iterationSystemMsgCount++;
2438
+ if (hasErrors) {
2439
+ // Always emit thinking event (SHADOW + BLOCKING)
2349
2440
  this.emitEvent({
2350
2441
  kind: "agent:thinking",
2351
2442
  content: `Auto-TSC: TypeScript errors found after editing ${tscFilesThisIteration.join(", ")}.`,
2352
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
+ }
2353
2455
  }
2354
- else if (!hasErrors) {
2456
+ else {
2355
2457
  this.emitEvent({
2356
2458
  kind: "agent:thinking",
2357
2459
  content: `Auto-TSC: No type errors after editing ${tscFilesThisIteration.length} file(s).`,
@@ -2418,8 +2520,9 @@ export class AgentLoop extends EventEmitter {
2418
2520
  // Reflection failure is non-fatal
2419
2521
  }
2420
2522
  }
2421
- // Level 1: Quick verification after every 3rd iteration
2422
- 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") {
2423
2526
  try {
2424
2527
  this.emitSubagent("verifier", "start", "running quick verification");
2425
2528
  const changedFilesMap = this.buildChangedFilesMap();
@@ -2457,6 +2560,8 @@ export class AgentLoop extends EventEmitter {
2457
2560
  const errorSummary = errorResults
2458
2561
  .map((r) => `${r.name}: ${r.output}`)
2459
2562
  .join("\n");
2563
+ // Track repeated error signature for Governor
2564
+ this.trackErrorSignature(errorSummary);
2460
2565
  // FailureRecovery: 근본 원인 분석 + 전략 선택
2461
2566
  const rootCause = this.failureRecovery.analyzeRootCause(errorSummary, errorResults[0]?.name);
2462
2567
  const decision = this.failureRecovery.selectStrategy(rootCause, {
@@ -2501,7 +2606,8 @@ export class AgentLoop extends EventEmitter {
2501
2606
  });
2502
2607
  // SelfDebugLoop: merge debug strategy into recovery message (not separate system msg)
2503
2608
  let debugSuffix = "";
2504
- if (iteration >= 3) {
2609
+ const llmFixerMode = this.overheadGovernor.shouldRunLlmFixer(this.buildTriggerContext());
2610
+ if (iteration >= 3 && llmFixerMode === "BLOCKING") {
2505
2611
  const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
2506
2612
  if (rootCauseAnalysis.confidence >= 0.5) {
2507
2613
  const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
@@ -2519,6 +2625,7 @@ export class AgentLoop extends EventEmitter {
2519
2625
  debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
2520
2626
  // Bug 2 fix: wire a real llmFixer so selfDebugLoop.debug() can call LLM
2521
2627
  if (debugStrategy !== "escalate") {
2628
+ this.llmFixerRunCount++;
2522
2629
  this.selfDebugLoop.debug({
2523
2630
  testCommand: testCmd,
2524
2631
  errorOutput: errorSummary,
@@ -2893,35 +3000,32 @@ export class AgentLoop extends EventEmitter {
2893
3000
  }
2894
3001
  // MCP 도구 호출 확인
2895
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
+ });
2896
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 };
2897
3014
  this.emitEvent({
2898
3015
  kind: "agent:tool_result",
2899
3016
  tool: toolCall.name,
2900
- output: mcpResult.output.length > 200
2901
- ? mcpResult.output.slice(0, 200) + "..."
2902
- : mcpResult.output,
2903
- durationMs: mcpResult.durationMs,
3017
+ output: finalResult.output.length > 200
3018
+ ? finalResult.output.slice(0, 200) + "..."
3019
+ : finalResult.output,
3020
+ durationMs: finalResult.durationMs,
2904
3021
  });
2905
3022
  this.emitEvent({ kind: "agent:reasoning_delta", text: `tool finished: ${toolCall.name}` });
2906
- return { result: mcpResult, deferredFixPrompt: null };
3023
+ return { result: finalResult, deferredFixPrompt: null };
2907
3024
  }
2908
3025
  // 도구 실행
2909
3026
  const startTime = Date.now();
2910
3027
  const toolAbort = new AbortController();
2911
3028
  this.interruptManager.registerToolAbort(toolAbort);
2912
- // Sandbox preflight — block disallowed tool calls before execution
2913
- if (this.sandbox) {
2914
- const sandboxCheck = this.sandbox.validateToolCall(toolCall.name, args);
2915
- if (!sandboxCheck.allowed) {
2916
- this.interruptManager.clearToolAbort();
2917
- const blocked = `[SANDBOX BLOCKED] ${sandboxCheck.violations.join("; ")}`;
2918
- this.emitEvent({ kind: "agent:tool_result", tool: toolCall.name, output: blocked, durationMs: 0 });
2919
- return {
2920
- result: { tool_call_id: toolCall.id, name: toolCall.name, output: blocked, success: false, durationMs: 0 },
2921
- deferredFixPrompt: null,
2922
- };
2923
- }
2924
- }
2925
3029
  if (["file_write", "file_edit"].includes(toolCall.name)) {
2926
3030
  const candidatePath = args.path ??
2927
3031
  args.file;
@@ -2969,29 +3073,21 @@ export class AgentLoop extends EventEmitter {
2969
3073
  });
2970
3074
  this.emitReasoning(`success: ${toolCall.name}`);
2971
3075
  this.reasoningTree.add("tool", `success: ${toolCall.name}`);
2972
- // Security Scanner postflight — scan written files for vulnerabilities (opt-in)
2973
- if (this.securityScanner && result.success && ["file_write", "file_edit"].includes(toolCall.name)) {
2974
- const writtenPath = args.path ?? args.file;
2975
- if (typeof writtenPath === "string") {
2976
- this.securityScanner.scan([writtenPath]).then((scanResult) => {
2977
- // Always render table — shows ✓ when clean, ⚠ when findings exist
2978
- const hasBad = scanResult.findings.some((f) => f.severity === "critical" || f.severity === "high");
2979
- if (hasBad || scanResult.findings.length > 0) {
2980
- this.emitEvent({
2981
- kind: "agent:text_delta",
2982
- text: formatSecurityTable(scanResult.findings, writtenPath),
2983
- });
2984
- }
2985
- }).catch(() => { });
2986
- }
2987
- }
2988
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
+ }
2989
3081
  const filePath = args.path ??
2990
3082
  args.file ??
2991
3083
  "unknown";
2992
3084
  const filePathStr = String(filePath);
2993
3085
  if (!this.changedFiles.includes(filePathStr)) {
2994
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
+ }
2995
3091
  }
2996
3092
  // Task 2: track write tool paths per-iteration for QA triggering
2997
3093
  if (!this.iterationWriteToolPaths.includes(filePathStr)) {
@@ -3002,6 +3098,8 @@ export class AgentLoop extends EventEmitter {
3002
3098
  this.iterationTsFilesModified.push(filePathStr);
3003
3099
  }
3004
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(() => { });
3005
3103
  // Update world state after file modification
3006
3104
  if (this.config.loop.projectPath) {
3007
3105
  const wsProjectPath = this.config.loop.projectPath;
@@ -3709,6 +3807,44 @@ export class AgentLoop extends EventEmitter {
3709
3807
  const args = this.parseToolArgs(toolCall.arguments);
3710
3808
  return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
3711
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
+ }
3712
3848
  /**
3713
3849
  * ContinuousReflection overflow signal을 soft rollover로 처리한다.
3714
3850
  * 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
@@ -3738,6 +3874,7 @@ export class AgentLoop extends EventEmitter {
3738
3874
  // cleanup failure ignored
3739
3875
  }
3740
3876
  }
3877
+ this.traceRecorder?.stop();
3741
3878
  }
3742
3879
  // ─── Helpers ───
3743
3880
  /**
@@ -3758,6 +3895,8 @@ export class AgentLoop extends EventEmitter {
3758
3895
  }
3759
3896
  emitEvent(event) {
3760
3897
  this.emit("event", event);
3898
+ // Trace recording — fire-and-forget, never blocks
3899
+ this.traceRecorder?.record(event);
3761
3900
  }
3762
3901
  emitReasoning(content) {
3763
3902
  this.reasoningTree.add("reasoning", content);
@@ -3770,6 +3909,13 @@ export class AgentLoop extends EventEmitter {
3770
3909
  emitSubagent(name, phase, content) {
3771
3910
  this.reasoningTree.add(name, `[${phase}] ${content}`);
3772
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
+ }
3773
3919
  }
3774
3920
  handleFatalError(err) {
3775
3921
  const message = err instanceof Error ? err.message : String(err);
@@ -3783,5 +3929,221 @@ export class AgentLoop extends EventEmitter {
3783
3929
  });
3784
3930
  return { reason: "ERROR", error: message };
3785
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;
3786
4148
  }
3787
4149
  //# sourceMappingURL=agent-loop.js.map