@yuaone/core 0.9.43 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/agent-affordance.d.ts +37 -0
  2. package/dist/agent-affordance.d.ts.map +1 -0
  3. package/dist/agent-affordance.js +139 -0
  4. package/dist/agent-affordance.js.map +1 -0
  5. package/dist/agent-decision-types.d.ts +262 -0
  6. package/dist/agent-decision-types.d.ts.map +1 -0
  7. package/dist/agent-decision-types.js +19 -0
  8. package/dist/agent-decision-types.js.map +1 -0
  9. package/dist/agent-decision.d.ts +52 -0
  10. package/dist/agent-decision.d.ts.map +1 -0
  11. package/dist/agent-decision.js +767 -0
  12. package/dist/agent-decision.js.map +1 -0
  13. package/dist/agent-loop.d.ts +37 -79
  14. package/dist/agent-loop.d.ts.map +1 -1
  15. package/dist/agent-loop.js +730 -586
  16. package/dist/agent-loop.js.map +1 -1
  17. package/dist/agent-reasoning-engine.d.ts +48 -0
  18. package/dist/agent-reasoning-engine.d.ts.map +1 -0
  19. package/dist/agent-reasoning-engine.js +544 -0
  20. package/dist/agent-reasoning-engine.js.map +1 -0
  21. package/dist/codebase-context.d.ts +3 -0
  22. package/dist/codebase-context.d.ts.map +1 -1
  23. package/dist/codebase-context.js +15 -6
  24. package/dist/codebase-context.js.map +1 -1
  25. package/dist/command-plan-compiler.d.ts +43 -0
  26. package/dist/command-plan-compiler.d.ts.map +1 -0
  27. package/dist/command-plan-compiler.js +164 -0
  28. package/dist/command-plan-compiler.js.map +1 -0
  29. package/dist/dependency-guard.d.ts +18 -0
  30. package/dist/dependency-guard.d.ts.map +1 -0
  31. package/dist/dependency-guard.js +113 -0
  32. package/dist/dependency-guard.js.map +1 -0
  33. package/dist/execution-engine.d.ts +10 -1
  34. package/dist/execution-engine.d.ts.map +1 -1
  35. package/dist/execution-engine.js +162 -8
  36. package/dist/execution-engine.js.map +1 -1
  37. package/dist/execution-receipt.d.ts +62 -0
  38. package/dist/execution-receipt.d.ts.map +1 -0
  39. package/dist/execution-receipt.js +67 -0
  40. package/dist/execution-receipt.js.map +1 -0
  41. package/dist/failure-surface-writer.d.ts +13 -0
  42. package/dist/failure-surface-writer.d.ts.map +1 -0
  43. package/dist/failure-surface-writer.js +33 -0
  44. package/dist/failure-surface-writer.js.map +1 -0
  45. package/dist/file-chunker.d.ts +26 -0
  46. package/dist/file-chunker.d.ts.map +1 -0
  47. package/dist/file-chunker.js +103 -0
  48. package/dist/file-chunker.js.map +1 -0
  49. package/dist/image-observer.d.ts +22 -0
  50. package/dist/image-observer.d.ts.map +1 -0
  51. package/dist/image-observer.js +60 -0
  52. package/dist/image-observer.js.map +1 -0
  53. package/dist/index.d.ts +55 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +53 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/judgment-rules.d.ts +44 -0
  58. package/dist/judgment-rules.d.ts.map +1 -0
  59. package/dist/judgment-rules.js +185 -0
  60. package/dist/judgment-rules.js.map +1 -0
  61. package/dist/memory-decay.d.ts +41 -0
  62. package/dist/memory-decay.d.ts.map +1 -0
  63. package/dist/memory-decay.js +62 -0
  64. package/dist/memory-decay.js.map +1 -0
  65. package/dist/memory-manager.d.ts.map +1 -1
  66. package/dist/memory-manager.js +30 -0
  67. package/dist/memory-manager.js.map +1 -1
  68. package/dist/model-weakness-tracker.d.ts +42 -0
  69. package/dist/model-weakness-tracker.d.ts.map +1 -0
  70. package/dist/model-weakness-tracker.js +107 -0
  71. package/dist/model-weakness-tracker.js.map +1 -0
  72. package/dist/overhead-governor.d.ts +3 -1
  73. package/dist/overhead-governor.d.ts.map +1 -1
  74. package/dist/overhead-governor.js +5 -0
  75. package/dist/overhead-governor.js.map +1 -1
  76. package/dist/patch-scope-controller.d.ts +44 -0
  77. package/dist/patch-scope-controller.d.ts.map +1 -0
  78. package/dist/patch-scope-controller.js +107 -0
  79. package/dist/patch-scope-controller.js.map +1 -0
  80. package/dist/patch-transaction.d.ts +53 -0
  81. package/dist/patch-transaction.d.ts.map +1 -0
  82. package/dist/patch-transaction.js +119 -0
  83. package/dist/patch-transaction.js.map +1 -0
  84. package/dist/pre-write-validator.d.ts +29 -0
  85. package/dist/pre-write-validator.d.ts.map +1 -0
  86. package/dist/pre-write-validator.js +97 -0
  87. package/dist/pre-write-validator.js.map +1 -0
  88. package/dist/prompt-builder.d.ts +25 -0
  89. package/dist/prompt-builder.d.ts.map +1 -0
  90. package/dist/prompt-builder.js +93 -0
  91. package/dist/prompt-builder.js.map +1 -0
  92. package/dist/prompt-envelope.d.ts +40 -0
  93. package/dist/prompt-envelope.d.ts.map +1 -0
  94. package/dist/prompt-envelope.js +16 -0
  95. package/dist/prompt-envelope.js.map +1 -0
  96. package/dist/prompt-runtime.d.ts +66 -0
  97. package/dist/prompt-runtime.d.ts.map +1 -0
  98. package/dist/prompt-runtime.js +492 -0
  99. package/dist/prompt-runtime.js.map +1 -0
  100. package/dist/repo-capability-profile.d.ts +24 -0
  101. package/dist/repo-capability-profile.d.ts.map +1 -0
  102. package/dist/repo-capability-profile.js +113 -0
  103. package/dist/repo-capability-profile.js.map +1 -0
  104. package/dist/security-gate.d.ts +39 -0
  105. package/dist/security-gate.d.ts.map +1 -0
  106. package/dist/security-gate.js +121 -0
  107. package/dist/security-gate.js.map +1 -0
  108. package/dist/self-evaluation.d.ts +22 -0
  109. package/dist/self-evaluation.d.ts.map +1 -0
  110. package/dist/self-evaluation.js +43 -0
  111. package/dist/self-evaluation.js.map +1 -0
  112. package/dist/semantic-diff-reviewer.d.ts +28 -0
  113. package/dist/semantic-diff-reviewer.d.ts.map +1 -0
  114. package/dist/semantic-diff-reviewer.js +168 -0
  115. package/dist/semantic-diff-reviewer.js.map +1 -0
  116. package/dist/stall-detector.d.ts +26 -2
  117. package/dist/stall-detector.d.ts.map +1 -1
  118. package/dist/stall-detector.js +128 -3
  119. package/dist/stall-detector.js.map +1 -1
  120. package/dist/system-core.d.ts +27 -0
  121. package/dist/system-core.d.ts.map +1 -0
  122. package/dist/system-core.js +269 -0
  123. package/dist/system-core.js.map +1 -0
  124. package/dist/system-prompt.d.ts +4 -0
  125. package/dist/system-prompt.d.ts.map +1 -1
  126. package/dist/system-prompt.js +12 -218
  127. package/dist/system-prompt.js.map +1 -1
  128. package/dist/target-file-ranker.d.ts +38 -0
  129. package/dist/target-file-ranker.d.ts.map +1 -0
  130. package/dist/target-file-ranker.js +90 -0
  131. package/dist/target-file-ranker.js.map +1 -0
  132. package/dist/task-classifier.d.ts +6 -0
  133. package/dist/task-classifier.d.ts.map +1 -1
  134. package/dist/task-classifier.js +6 -0
  135. package/dist/task-classifier.js.map +1 -1
  136. package/dist/test-impact-planner.d.ts +16 -0
  137. package/dist/test-impact-planner.d.ts.map +1 -0
  138. package/dist/test-impact-planner.js +68 -0
  139. package/dist/test-impact-planner.js.map +1 -0
  140. package/dist/tool-outcome-cache.d.ts +41 -0
  141. package/dist/tool-outcome-cache.d.ts.map +1 -0
  142. package/dist/tool-outcome-cache.js +88 -0
  143. package/dist/tool-outcome-cache.js.map +1 -0
  144. package/dist/types.d.ts +39 -0
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/verifier-rules.d.ts +15 -0
  147. package/dist/verifier-rules.d.ts.map +1 -0
  148. package/dist/verifier-rules.js +80 -0
  149. package/dist/verifier-rules.js.map +1 -0
  150. package/dist/workspace-mutation-policy.d.ts +28 -0
  151. package/dist/workspace-mutation-policy.d.ts.map +1 -0
  152. package/dist/workspace-mutation-policy.js +56 -0
  153. package/dist/workspace-mutation-policy.js.map +1 -0
  154. package/package.json +1 -1
@@ -21,6 +21,8 @@ import { InterruptManager } from "./interrupt-manager.js";
21
21
  import { YuanMemory } from "./memory.js";
22
22
  import { MemoryManager } from "./memory-manager.js";
23
23
  import { buildSystemPrompt } from "./system-prompt.js";
24
+ import { compilePromptEnvelope } from "./prompt-runtime.js";
25
+ import { buildPrompt } from "./prompt-builder.js";
24
26
  import { HierarchicalPlanner, } from "./hierarchical-planner.js";
25
27
  import { TaskClassifier } from "./task-classifier.js";
26
28
  import { PromptDefense } from "./prompt-defense.js";
@@ -55,12 +57,14 @@ import { ContextBudgetManager } from "./context-budget.js";
55
57
  import { QAPipeline } from "./qa-pipeline.js";
56
58
  import { PersonaManager } from "./persona.js";
57
59
  import { InMemoryVectorStore, OllamaEmbeddingProvider } from "./vector-store.js";
60
+ import { agentDecide, DEFAULT_DECISION, worldStateToProjectContext } from "./agent-decision.js";
58
61
  import { OverheadGovernor } from "./overhead-governor.js";
59
62
  import { StateStore, TransitionModel, SimulationEngine, StateUpdater, } from "./world-model/index.js";
60
63
  import { MilestoneChecker, RiskEstimator, PlanEvaluator, ReplanningEngine, } from "./planner/index.js";
61
64
  import { TraceRecorder } from "./trace-recorder.js";
62
65
  import { ArchSummarizer } from "./arch-summarizer.js";
63
66
  import { FailureSignatureMemory } from "./failure-signature-memory.js";
67
+ import { CausalChainResolver } from "./causal-chain-resolver.js";
64
68
  import { PlaybookLibrary } from "./playbook-library.js";
65
69
  import { ProjectExecutive } from "./project-executive.js";
66
70
  import { StallDetector } from "./stall-detector.js";
@@ -81,37 +85,20 @@ import { registerToolsInGraph, recordToolOutcomeInGraph } from "./extensions/cap
81
85
  import { getSelfWeaknessContext } from "./extensions/self-model-wiring.js";
82
86
  import { initMarketPlaybooks, selectMarketStrategy } from "./extensions/strategy-wiring.js";
83
87
  import { VisionIntentDetector } from "./vision-intent-detector.js";
88
+ import { PatchTransactionJournal } from "./patch-transaction.js";
89
+ import { securityCheck } from "./security-gate.js";
90
+ import { JudgmentRuleRegistry } from "./judgment-rules.js";
91
+ import { loadOrScanProfile } from "./repo-capability-profile.js";
92
+ import { verifyToolResult } from "./verifier-rules.js";
93
+ import { checkDependencyChange } from "./dependency-guard.js";
94
+ import { WorkspaceMutationPolicy } from "./workspace-mutation-policy.js";
95
+ import { validateBeforeWrite, detectFileRole } from "./pre-write-validator.js";
96
+ import { PatchScopeController, detectRepoLifecycle } from "./patch-scope-controller.js";
97
+ import { classifyCommand, validateProposedCommand, compileVerifyCommands } from "./command-plan-compiler.js";
98
+ import { reviewFileDiff } from "./semantic-diff-reviewer.js";
99
+ import { ModelWeaknessTracker } from "./model-weakness-tracker.js";
84
100
  import { dlog, dlogSep } from "./debug-logger.js";
85
- /**
86
- * AgentLoop — YUAN 에이전트의 핵심 실행 루프.
87
- *
88
- * 동작 흐름:
89
- * 1. 사용자 메시지 수신
90
- * 2. 시스템 프롬프트 + 히스토리로 LLM 호출
91
- * 3. LLM 응답에서 tool_call 파싱
92
- * 4. Governor가 안전성 검증
93
- * 5. 도구 실행 → 결과를 히스토리에 추가
94
- * 6. LLM에 결과 피드백 → 2번으로 반복
95
- * 7. 종료 조건 충족 시 결과 반환
96
- *
97
- * @example
98
- * ```typescript
99
- * const loop = new AgentLoop({
100
- * config: agentConfig,
101
- * toolExecutor: executor,
102
- * governorConfig: { planTier: "PRO" },
103
- * });
104
- *
105
- * loop.on("event", (event: AgentEvent) => {
106
- * // SSE 스트리밍
107
- * });
108
- *
109
- * const result = await loop.run("모든 console.log를 제거해줘");
110
- * ```
111
- */
112
- /** Minimum confidence for classification-based hints/routing to activate */
113
101
  const CLASSIFICATION_CONFIDENCE_THRESHOLD = 0.6;
114
- /** spawn_sub_agent tool definition — allows the LLM to delegate tasks to sub-agents */
115
102
  const SPAWN_SUB_AGENT_TOOL = {
116
103
  name: "spawn_sub_agent",
117
104
  description: "Spawn a sub-agent to work on a delegated task in parallel. " +
@@ -167,14 +154,19 @@ export class AgentLoop extends EventEmitter {
167
154
  approvalManager;
168
155
  autoFixLoop;
169
156
  interruptManager;
157
+ /** @deprecated Use decision.core.memoryLoad.shouldLoad at runtime. Kept for constructor init only. */
170
158
  enableMemory;
159
+ /** @deprecated Use decision.core.planRequired at runtime. Kept for constructor init only. */
171
160
  enablePlanning;
161
+ /** @deprecated Planning threshold is now determined by Decision Engine. Kept for constructor init only. */
172
162
  planningThreshold;
173
163
  environment;
174
164
  yuanMemory = null;
175
165
  memoryManager = null;
166
+ judgmentRegistry = null;
176
167
  planner = null;
177
168
  activePlan = null;
169
+ /** @deprecated Use AgentDecisionContext when available; taskClassifier is legacy fallback only */
178
170
  taskClassifier;
179
171
  promptDefense;
180
172
  reflexionEngine = null;
@@ -205,6 +197,23 @@ export class AgentLoop extends EventEmitter {
205
197
  enableSelfReflection;
206
198
  enableDebate;
207
199
  currentComplexity = "simple";
200
+ // ─── Agent Decision Engine ─────────────────────────────────────────────────
201
+ decision = DEFAULT_DECISION;
202
+ // ─── Prompt 3-Layer Architecture ──────────────────────────────────────────
203
+ pendingRunContext = {};
204
+ /** Cached values for refreshSystemPrompt() — set during criticalInit/backgroundInit */
205
+ _cachedProjectStructure;
206
+ _cachedYuanMdContent;
207
+ prevDecision = DEFAULT_DECISION;
208
+ toolUsageCounter = {
209
+ reads: 0,
210
+ edits: 0,
211
+ shells: 0,
212
+ tests: 0,
213
+ searches: 0,
214
+ webLookups: 0,
215
+ sameFileEdits: new Map(),
216
+ };
208
217
  policyOverrides;
209
218
  checkpointSaved = false;
210
219
  iterationCount = 0;
@@ -224,33 +233,35 @@ export class AgentLoop extends EventEmitter {
224
233
  backgroundAgentManager = null;
225
234
  sessionPersistence = null;
226
235
  sessionId = null;
236
+ /** @deprecated Use decision.core.skillActivation.enableToolPlanning at runtime. Kept for constructor init only. */
227
237
  enableToolPlanning;
238
+ /** @deprecated Use decision.core.skillActivation.enableSkillLearning at runtime. Kept for constructor init only. */
228
239
  enableSkillLearning;
240
+ /** @deprecated Use decision.core.subAgentPlan.enabled at runtime. Kept for constructor init only. */
229
241
  enableBackgroundAgents;
230
242
  currentToolPlan = null;
231
243
  executedToolNames = [];
232
244
  /** Unfulfilled-intent continuation counter — reset each run(), max 3 nudges */
233
245
  _unfulfilledContinuations = 0;
246
+ /** Model weakness learning layer — scoped by model+repo */
247
+ weaknessTracker = null;
234
248
  /** Context Budget: max 3 active skills at once */
235
249
  activeSkillIds = [];
236
250
  static MAX_ACTIVE_SKILLS = 3;
237
251
  /** Context Budget: track injected system messages per iteration to cap at 5 */
238
252
  iterationSystemMsgCount = 0;
239
- /** Task 1: ContextBudgetManager for LLM-based summarization at 60-70% context usage */
240
253
  contextBudgetManager = null;
241
- /** Task 1: Flag to ensure LLM summarization only runs once per agent run (non-blocking guard) */
242
254
  _contextSummarizationDone = false;
243
- /** Task 2: Track whether write tools ran this iteration for QA triggering */
244
255
  iterationWriteToolPaths = [];
245
- /** Task 2: Last QA result (surfaced to LLM on issues) */
246
256
  lastQAResult = null;
247
- /** Task 3: Track TS files modified this run for auto-tsc */
248
257
  iterationTsFilesModified = [];
249
- /** Task 3: Whether tsc was run in the previous iteration (skip cooldown) */
250
258
  tscRanLastIteration = false;
251
- // Removed aggressive recovery fields (_lastToolFailed, _toolRetryCount, _consecutiveTextOnlyIterations)
252
- // they caused recovery loops. Model decides retry on its own.
253
- // ─── OverheadGovernor — subsystem execution policy ──────────────────────
259
+ patchJournal = null;
260
+ mutationPolicy = null;
261
+ patchScopeController = null;
262
+ /** Per-iteration ephemeral hints — collected and bulk-injected before next LLM call */
263
+ ephemeralHints = [];
264
+ // ─── OverheadGovernor ──────────────────────────────────────────────────
254
265
  overheadGovernor;
255
266
  /** Writes since last verify ran (for QA/quickVerify trigger) */
256
267
  writeCountSinceVerify = 0;
@@ -303,14 +314,11 @@ export class AgentLoop extends EventEmitter {
303
314
  selfImprovementLoop = null;
304
315
  metaLearningCollector = null;
305
316
  trustEconomics = null;
306
- // Phase 5: Strategy Learner + Skill Registry
307
317
  strategyLearner = null;
308
318
  skillRegistry = null;
309
- // Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
310
319
  tracePatternExtractor = null;
311
320
  metaLearningEngine = null;
312
321
  toolSynthesizer = null;
313
- // Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
314
322
  budgetGovernorV2 = null;
315
323
  capabilityGraph = null;
316
324
  capabilitySelfModel = null;
@@ -385,30 +393,23 @@ export class AgentLoop extends EventEmitter {
385
393
  this.environment = options.environment;
386
394
  this.enableSelfReflection = options.enableSelfReflection !== false;
387
395
  this.enableDebate = options.enableDebate !== false;
388
- // BYOK LLM 클라이언트 생성
389
396
  this.llmClient = new BYOKClient(options.config.byok);
390
- // Governor 생성
391
397
  this.governor = new Governor(options.governorConfig);
392
- // OverheadGovernor — subsystem execution policy
393
398
  this.overheadGovernor = new OverheadGovernor(options.overheadGovernorConfig, (subsystem, reason) => {
394
399
  // Shadow log — emit as thinking event so TUI can see it (dimmed)
395
400
  this.emitEvent({ kind: "agent:thinking", content: `[shadow] ${subsystem}: ${reason}` });
396
401
  });
397
- // ContextManager 생성
398
402
  this.contextManager = new ContextManager({
399
403
  maxContextTokens: options.contextConfig?.maxContextTokens ??
400
404
  options.config.loop.totalTokenBudget,
401
405
  outputReserveTokens: options.contextConfig?.outputReserveTokens ?? 4096,
402
406
  ...options.contextConfig,
403
407
  });
404
- // ApprovalManager 생성
405
408
  this.approvalManager = new ApprovalManager(options.approvalConfig);
406
409
  if (options.approvalHandler) {
407
410
  this.approvalManager.setHandler(options.approvalHandler);
408
411
  }
409
- // AutoFixLoop 생성
410
412
  this.autoFixLoop = new AutoFixLoop(options.autoFixConfig);
411
- // Task Classifier + Prompt Defense + Token Budget + Memory Updater
412
413
  this.taskClassifier = new TaskClassifier();
413
414
  this.promptDefense = new PromptDefense();
414
415
  this.tokenBudgetManager = new TokenBudgetManager({
@@ -418,22 +419,17 @@ export class AgentLoop extends EventEmitter {
418
419
  this.failureRecovery = new FailureRecovery();
419
420
  this.costOptimizer = new CostOptimizer();
420
421
  this.policyOverrides = options.policyOverrides;
421
- // MCP 서버 설정 저장
422
422
  this.mcpServerConfigs = options.mcpServerConfigs ?? [];
423
- // InterruptManager 설정 (외부 주입 또는 내부 생성)
424
423
  this.interruptManager = options.interruptManager ?? new InterruptManager();
425
424
  this.setupInterruptListeners();
426
- // PluginRegistry + SkillLoader 초기화
427
425
  this.pluginRegistry = options.pluginRegistry ?? new PluginRegistry();
428
426
  this.skillLoader = new SkillLoader();
429
- // Advanced Intelligence 모듈 초기화
430
427
  this.specialistRegistry = new SpecialistRegistry();
431
428
  this.toolPlanner = new ToolPlanner();
432
429
  this.selfDebugLoop = new SelfDebugLoop();
433
430
  this.enableToolPlanning = options.enableToolPlanning !== false;
434
431
  this.enableSkillLearning = options.enableSkillLearning !== false;
435
432
  this.enableBackgroundAgents = options.enableBackgroundAgents === true;
436
- // 시스템 프롬프트 추가 (메모리 없이 기본 프롬프트로 시작, init()에서 갱신)
437
433
  this.contextManager.addMessage({
438
434
  role: "system",
439
435
  content: this.config.loop.systemPrompt,
@@ -464,7 +460,6 @@ export class AgentLoop extends EventEmitter {
464
460
  if (this.partialInit)
465
461
  return;
466
462
  this.partialInit = true;
467
- // ContextBudgetManager 초기화
468
463
  this.contextBudgetManager = new ContextBudgetManager({
469
464
  totalBudget: this.config.loop.totalTokenBudget,
470
465
  enableSummarization: true,
@@ -473,7 +468,6 @@ export class AgentLoop extends EventEmitter {
473
468
  const projectPath = this.config.loop.projectPath;
474
469
  if (!projectPath)
475
470
  return;
476
- // Session persistence init
477
471
  try {
478
472
  this.sessionPersistence = new SessionPersistence(undefined, projectPath);
479
473
  }
@@ -481,19 +475,22 @@ export class AgentLoop extends EventEmitter {
481
475
  this.sessionPersistence = null;
482
476
  }
483
477
  let yuanMdContent;
484
- // Memory 로드 (YUAN.md + MemoryManager + PersonaManager — fast file I/O only)
485
478
  if (this.enableMemory) {
486
479
  try {
487
- // YUAN.md (raw markdown)
488
480
  this.yuanMemory = new YuanMemory(projectPath);
489
481
  const memData = await this.yuanMemory.load();
490
482
  if (memData) {
491
483
  yuanMdContent = memData.raw;
492
484
  }
493
- // MemoryManager (structured learnings)
494
485
  this.memoryManager = new MemoryManager(projectPath);
495
486
  await this.memoryManager.load();
496
- // PersonaManager user communication style learning + persona injection
487
+ this.judgmentRegistry = new JudgmentRuleRegistry(projectPath);
488
+ // Auto-expand judgment rules from repo capability profile
489
+ try {
490
+ const repoProfile = loadOrScanProfile(projectPath);
491
+ this.judgmentRegistry.autoExpandFromProfile(repoProfile);
492
+ }
493
+ catch { /* non-fatal */ }
497
494
  const personaUserId = basename(projectPath) || "default";
498
495
  this.personaManager = new PersonaManager({
499
496
  userId: personaUserId,
@@ -503,7 +500,6 @@ export class AgentLoop extends EventEmitter {
503
500
  await this.personaManager.loadProfile().catch(() => { });
504
501
  }
505
502
  catch (memErr) {
506
- // 메모리 로드 실패는 치명적이지 않음 — 경고만 출력
507
503
  this.emitEvent({
508
504
  kind: "agent:error",
509
505
  message: `Memory load failed: ${memErr instanceof Error ? memErr.message : String(memErr)}`,
@@ -511,7 +507,6 @@ export class AgentLoop extends EventEmitter {
511
507
  });
512
508
  }
513
509
  }
514
- // ExecutionPolicyEngine 로드
515
510
  try {
516
511
  this.policyEngine = new ExecutionPolicyEngine(projectPath);
517
512
  await this.policyEngine.load();
@@ -520,16 +515,13 @@ export class AgentLoop extends EventEmitter {
520
515
  this.policyEngine.override(section, values);
521
516
  }
522
517
  }
523
- // FailureRecovery에 정책 적용
524
518
  const recoveryConfig = this.policyEngine.toFailureRecoveryConfig();
525
519
  this.failureRecovery = new FailureRecovery(recoveryConfig);
526
520
  }
527
521
  catch {
528
- // 정책 로드 실패 기본값 사용
522
+ // Policy load failure use defaults
529
523
  }
530
- // ImpactAnalyzer 생성
531
524
  this.impactAnalyzer = new ImpactAnalyzer({ projectPath });
532
- // ContinuationEngine 생성 + 체크포인트 복원
533
525
  this.continuationEngine = new ContinuationEngine({ projectPath });
534
526
  try {
535
527
  const latestCheckpoint = await this.continuationEngine.findLatestCheckpoint();
@@ -539,39 +531,53 @@ export class AgentLoop extends EventEmitter {
539
531
  role: "system",
540
532
  content: continuationPrompt,
541
533
  });
542
- // 복원 후 체크포인트 정리
543
534
  await this.continuationEngine.pruneOldCheckpoints();
544
535
  }
545
536
  }
546
537
  catch {
547
- // 체크포인트 복원 실패는 치명적이지 않음
548
- }
549
- // 향상된 시스템 프롬프트 생성 (projectStructure/worldState는 background에서 보완)
550
- const enhancedPrompt = buildSystemPrompt({
551
- projectStructure: undefined, // background에서 analyzeProject() 후 갱신
552
- yuanMdContent,
553
- tools: [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL],
554
- projectPath,
555
- environment: this.environment,
556
- });
557
- // 기존 시스템 메시지를 향상된 프롬프트로 교체
558
- this.contextManager.replaceSystemMessage(enhancedPrompt);
559
- // MemoryManager의 관련 학습/경고를 추가 컨텍스트로 주입
538
+ // non-fatal
539
+ }
540
+ this._cachedYuanMdContent = yuanMdContent;
541
+ this._cachedProjectStructure = undefined;
542
+ const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
543
+ try {
544
+ const envelope = compilePromptEnvelope({
545
+ decision: this.decision,
546
+ promptOptions: {
547
+ projectStructure: undefined, // background에서 analyzeProject() 후 갱신
548
+ yuanMdContent,
549
+ tools: allTools,
550
+ activeToolNames: allTools.map(t => t.name),
551
+ projectPath,
552
+ environment: this.environment,
553
+ },
554
+ runContext: this.pendingRunContext,
555
+ });
556
+ const enhancedPrompt = buildPrompt(envelope);
557
+ this.contextManager.replaceSystemMessage(enhancedPrompt);
558
+ }
559
+ catch {
560
+ // Fallback to legacy buildSystemPrompt if PromptRuntime fails
561
+ const enhancedPrompt = buildSystemPrompt({
562
+ projectStructure: undefined,
563
+ yuanMdContent,
564
+ tools: allTools,
565
+ projectPath,
566
+ environment: this.environment,
567
+ });
568
+ this.contextManager.replaceSystemMessage(enhancedPrompt);
569
+ }
570
+ // MemoryManager learnings → pendingRunContext
560
571
  if (this.memoryManager) {
561
572
  const memory = this.memoryManager.getMemory();
562
573
  if (memory.learnings.length > 0 || memory.failedApproaches.length > 0) {
563
574
  const memoryContext = this.buildMemoryContext(memory);
564
575
  if (memoryContext) {
565
- this.contextManager.addMessage({
566
- role: "system",
567
- content: memoryContext,
568
- });
576
+ this.pendingRunContext.memoryContext = memoryContext;
577
+ this.refreshSystemPrompt();
569
578
  }
570
579
  }
571
580
  }
572
- // criticalInit 완료 — LLM 호출 가능 상태
573
- // initialized=true는 backgroundInit() 완료 후 설정됨
574
- // partialInit은 backgroundInit이 시작될 때까지 유지
575
581
  }
576
582
  /**
577
583
  * TTFT 최적화: LLM 호출을 블로킹하지 않는 백그라운드 초기화.
@@ -583,8 +589,6 @@ export class AgentLoop extends EventEmitter {
583
589
  * criticalInit() 완료 후 fire-and-forget으로 실행됨.
584
590
  */
585
591
  async backgroundInit() {
586
- // backgroundInit은 criticalInit이 완료된 후에만 의미 있음
587
- // initialized=true이면 이미 완료된 것
588
592
  if (this.initialized)
589
593
  return;
590
594
  const projectPath = this.config.loop.projectPath;
@@ -593,9 +597,7 @@ export class AgentLoop extends EventEmitter {
593
597
  this.partialInit = false;
594
598
  return;
595
599
  }
596
- // partialInit을 false로 — backgroundInit 실행 중이므로 재진입 방어 불필요
597
600
  this.partialInit = false;
598
- // VectorStore — RAG semantic code context (Ollama 느릴 수 있음)
599
601
  if (this.enableMemory && this.yuanMemory) {
600
602
  try {
601
603
  const personaUserId = basename(projectPath) || "default";
@@ -608,30 +610,21 @@ export class AgentLoop extends EventEmitter {
608
610
  this.vectorStore.load(),
609
611
  new Promise(resolve => setTimeout(resolve, 1_500)),
610
612
  ]).catch(() => { });
611
- // Background code indexing — fire and forget
612
613
  const vectorStoreRef = this.vectorStore;
613
614
  import("./code-indexer.js").then(({ CodeIndexer }) => {
614
615
  const indexer = new CodeIndexer({});
615
616
  indexer.indexProject(projectPath, vectorStoreRef).catch(() => { });
616
617
  }).catch(() => { });
617
- // 프로젝트 구조 분석 (느림 — file scan)
618
618
  const projectStructure = await this.yuanMemory.analyzeProject();
619
- // analyzeProject 완료 후 시스템 프롬프트 재빌드 (WorldState 아직 없어도 됨)
620
619
  const yuanMdContent = (await this.yuanMemory.load().catch(() => null))?.raw;
621
- const updatedPrompt = buildSystemPrompt({
622
- projectStructure,
623
- yuanMdContent,
624
- tools: [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL],
625
- projectPath,
626
- environment: this.environment,
627
- });
628
- this.contextManager.replaceSystemMessage(updatedPrompt);
620
+ this._cachedProjectStructure = projectStructure;
621
+ this._cachedYuanMdContent = yuanMdContent;
622
+ this.refreshSystemPrompt();
629
623
  }
630
624
  catch {
631
625
  // non-fatal
632
626
  }
633
627
  }
634
- // WorldState 수집 → system prompt에 주입
635
628
  try {
636
629
  const worldStateCollector = new WorldStateCollector({
637
630
  projectPath,
@@ -639,22 +632,17 @@ export class AgentLoop extends EventEmitter {
639
632
  skipTest: true,
640
633
  });
641
634
  this.worldState = await worldStateCollector.collect();
642
- // WorldState 섹션 시스템 프롬프트에 추가
643
635
  if (this.worldState) {
644
636
  const collector = new WorldStateCollector({ projectPath });
645
- const worldStateSection = "\n\n" + collector.formatForPrompt(this.worldState);
646
- // 현재 시스템 메시지에 WorldState 추가
647
- const currentMsgs = this.contextManager.getMessages();
648
- const sysMsg = currentMsgs.find((m) => m.role === "system");
649
- if (sysMsg && !String(sysMsg.content).includes("## Current Environment")) {
650
- this.contextManager.replaceSystemMessage(String(sysMsg.content) + worldStateSection);
651
- }
637
+ const worldStateSection = collector.formatForPrompt(this.worldState);
638
+ this.pendingRunContext.worldStateSection = worldStateSection;
639
+ this.refreshSystemPrompt();
652
640
  }
653
641
  }
654
642
  catch {
655
- // WorldState 수집 실패는 치명적이지 않음
643
+ // non-fatal
656
644
  }
657
- // CodeOrchestrator: 언어별 코딩 표준 + 프로젝트 컨텍스트 주입
645
+ // CodeOrchestrator append directly (not yet in runContext schema)
658
646
  try {
659
647
  const { codeOrchestrator } = await import('./code-orchestrator.js');
660
648
  const codeCtx = await codeOrchestrator.getContextForLLM(projectPath ?? '');
@@ -667,7 +655,6 @@ export class AgentLoop extends EventEmitter {
667
655
  }
668
656
  }
669
657
  catch { /* non-fatal */ }
670
- // Initialize World Model
671
658
  if (this.worldState && projectPath) {
672
659
  try {
673
660
  this.transitionModel = new TransitionModel();
@@ -675,11 +662,8 @@ export class AgentLoop extends EventEmitter {
675
662
  this.simulationEngine = new SimulationEngine(this.transitionModel, this.worldModel);
676
663
  this.stateUpdater = new StateUpdater(this.worldModel, projectPath);
677
664
  }
678
- catch {
679
- // World Model initialization failure is non-fatal
680
- }
665
+ catch { /* non-fatal */ }
681
666
  }
682
- // Capture last known good git commit for FailureRecovery rollback
683
667
  try {
684
668
  const headHash = execSync("git rev-parse HEAD", {
685
669
  cwd: projectPath,
@@ -691,9 +675,8 @@ export class AgentLoop extends EventEmitter {
691
675
  }
692
676
  }
693
677
  catch {
694
- // Not a git repo or git unavailable — FailureRecovery will use file-level rollback only
678
+ // non-fatal — FailureRecovery will use file-level rollback
695
679
  }
696
- // MCP 클라이언트 연결 (connectAll은 네트워크 I/O라 느릴 수 있음)
697
680
  {
698
681
  let mergedMCPConfigs = [...this.mcpServerConfigs];
699
682
  try {
@@ -732,9 +715,7 @@ export class AgentLoop extends EventEmitter {
732
715
  }
733
716
  }
734
717
  }
735
- // ReflexionEngine 생성
736
718
  this.reflexionEngine = new ReflexionEngine({ projectPath });
737
- // SelfReflection 생성 (6D deep verify + quick verify)
738
719
  if (this.enableSelfReflection) {
739
720
  const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
740
721
  this.selfReflection = new SelfReflection(sessionId, {
@@ -749,12 +730,9 @@ export class AgentLoop extends EventEmitter {
749
730
  const memory = await this.memoryManager.load();
750
731
  this.selfReflection.loadFromMemory(memory);
751
732
  }
752
- catch {
753
- // 학습 복원 실패는 치명적이지 않음
754
- }
733
+ catch { /* non-fatal */ }
755
734
  }
756
735
  }
757
- // DebateOrchestrator 생성 (complex/massive 태스크에서 multi-agent debate)
758
736
  if (this.enableDebate && projectPath) {
759
737
  this.debateOrchestrator = new DebateOrchestrator({
760
738
  projectPath,
@@ -767,13 +745,31 @@ export class AgentLoop extends EventEmitter {
767
745
  totalTokenBudget: Math.floor(this.config.loop.totalTokenBudget * 0.3),
768
746
  });
769
747
  }
770
- // Self-model weakness context injection
771
748
  const weaknessCtx = getSelfWeaknessContext(this.capabilitySelfModel);
772
749
  if (weaknessCtx) {
773
- this.contextManager.addMessage({ role: "system", content: weaknessCtx });
750
+ this.pendingRunContext.weaknessContext = weaknessCtx;
751
+ this.refreshSystemPrompt();
752
+ }
753
+ // ModelWeaknessTracker — learn from repeated validator blocks
754
+ if (projectPath) {
755
+ try {
756
+ const modelId = this.config.byok.model ?? this.config.byok.provider;
757
+ this.weaknessTracker = new ModelWeaknessTracker(projectPath, modelId);
758
+ const hints = this.weaknessTracker.getPreventiveHints();
759
+ if (hints.length > 0) {
760
+ const existing = this.pendingRunContext.weaknessContext ?? "";
761
+ this.pendingRunContext.weaknessContext =
762
+ (existing ? existing + "\n" : "") +
763
+ "[Model Weakness Prevention]\n" + hints.map(h => `- ${h}`).join("\n");
764
+ this.refreshSystemPrompt();
765
+ }
766
+ }
767
+ catch {
768
+ this.weaknessTracker = null;
769
+ }
774
770
  }
775
- // SkillLearner 초기화 (경험에서 학습된 스킬 자동 로드 — slow disk I/O)
776
- if (this.enableSkillLearning && projectPath) {
771
+ const shouldLearnSkills = this.decision.core.skillActivation.enableSkillLearning;
772
+ if (shouldLearnSkills && projectPath) {
777
773
  try {
778
774
  this.skillLearner = new SkillLearner(projectPath);
779
775
  await this.skillLearner.init();
@@ -782,7 +778,6 @@ export class AgentLoop extends EventEmitter {
782
778
  this.skillLearner = null;
783
779
  }
784
780
  }
785
- // Phase 4: FailureSignatureMemory, PlaybookLibrary, ProjectExecutive, StallDetector
786
781
  if (projectPath) {
787
782
  try {
788
783
  this.failureSigMemory = new FailureSignatureMemory({ projectPath });
@@ -796,21 +791,15 @@ export class AgentLoop extends EventEmitter {
796
791
  this.metaLearningCollector = new MetaLearningCollector({ projectPath });
797
792
  this.trustEconomics = new TrustEconomics({ projectPath });
798
793
  }
799
- catch {
800
- // Phase 4 init failure is non-fatal
801
- }
794
+ catch { /* non-fatal */ }
802
795
  }
803
- // Phase 5: StrategyLearner + SkillRegistry
804
796
  try {
805
797
  this.strategyLearner = new StrategyLearner();
806
798
  this.skillRegistry = new SkillRegistry();
807
799
  this.strategyLearner.on("event", (ev) => this.emitEvent(ev));
808
800
  this.skillRegistry.on("event", (ev) => this.emitEvent(ev));
809
801
  }
810
- catch {
811
- // Phase 5 init failure is non-fatal
812
- }
813
- // Phase 5 extended: TracePatternExtractor, MetaLearningEngine, ToolSynthesizer
802
+ catch { /* non-fatal */ }
814
803
  try {
815
804
  this.tracePatternExtractor = new TracePatternExtractor();
816
805
  this.metaLearningEngine = new MetaLearningEngine({
@@ -822,10 +811,7 @@ export class AgentLoop extends EventEmitter {
822
811
  this.metaLearningEngine.on("event", (ev) => this.emitEvent(ev));
823
812
  this.toolSynthesizer.on("event", (ev) => this.emitEvent(ev));
824
813
  }
825
- catch {
826
- // Phase 5 extended init failure is non-fatal
827
- }
828
- // Phase 6: BudgetGovernorV2, CapabilityGraph, CapabilitySelfModel, StrategyMarket
814
+ catch { /* non-fatal */ }
829
815
  try {
830
816
  this.budgetGovernorV2 = new BudgetGovernorV2({ taskBudget: this.config.loop.totalTokenBudget || 200_000 });
831
817
  this.capabilityGraph = new CapabilityGraph();
@@ -839,12 +825,10 @@ export class AgentLoop extends EventEmitter {
839
825
  registerToolsInGraph(this.capabilityGraph, toolNames);
840
826
  }
841
827
  catch { /* non-fatal */ }
842
- // HierarchicalPlanner 생성
843
828
  this.planner = new HierarchicalPlanner({ projectPath });
844
829
  if (this.skillLearner) {
845
830
  this.planner.setSkillLearner(this.skillLearner);
846
831
  }
847
- // Initialize Proactive Replanning (requires planner + impactAnalyzer + worldModel)
848
832
  try {
849
833
  if (this.impactAnalyzer && this.worldModel && this.simulationEngine && this.transitionModel) {
850
834
  this.milestoneChecker = new MilestoneChecker();
@@ -853,9 +837,7 @@ export class AgentLoop extends EventEmitter {
853
837
  this.replanningEngine = new ReplanningEngine(this.planner, this.planEvaluator, this.riskEstimator, this.milestoneChecker);
854
838
  }
855
839
  }
856
- catch {
857
- // Proactive replanning initialization failure is non-fatal
858
- }
840
+ catch { /* non-fatal */ }
859
841
  if (this.skillLearner) {
860
842
  const learnedSkills = this.skillLearner.getAllSkills();
861
843
  if (learnedSkills.length > 0) {
@@ -863,14 +845,11 @@ export class AgentLoop extends EventEmitter {
863
845
  .filter((s) => s.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD)
864
846
  .map((s) => s.id);
865
847
  if (skillNames.length > 0) {
866
- this.contextManager.addMessage({
867
- role: "system",
868
- content: `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`,
869
- });
848
+ this.pendingRunContext.learnedSkills = `[Learned Skills: ${skillNames.join(", ")}] — Auto-activate on matching error patterns.`;
849
+ this.refreshSystemPrompt();
870
850
  }
871
851
  }
872
852
  }
873
- // RepoKnowledgeGraph 초기화 (코드 구조 그래프 — 비동기 빌드)
874
853
  try {
875
854
  this.repoGraph = new RepoKnowledgeGraph(projectPath);
876
855
  this.repoGraph.buildFromProject(projectPath).catch(() => { });
@@ -878,8 +857,8 @@ export class AgentLoop extends EventEmitter {
878
857
  catch {
879
858
  this.repoGraph = null;
880
859
  }
881
- // BackgroundAgentManager 초기화 (opt-in)
882
- if (this.enableBackgroundAgents && projectPath) {
860
+ const shouldEnableBgAgents = this.decision.core.subAgentPlan.enabled;
861
+ if (shouldEnableBgAgents && projectPath) {
883
862
  try {
884
863
  this.backgroundAgentManager = new BackgroundAgentManager();
885
864
  this.backgroundAgentManager.createDefaults(projectPath);
@@ -903,18 +882,14 @@ export class AgentLoop extends EventEmitter {
903
882
  this.backgroundAgentManager = null;
904
883
  }
905
884
  }
906
- // Inject active plugin skills into system prompt (lazy: names only)
907
885
  const activeSkills = this.pluginRegistry.getAllSkills();
908
886
  if (activeSkills.length > 0) {
909
887
  const skillSummary = activeSkills
910
888
  .map((s) => `${s.pluginId}/${s.skill.name}`)
911
889
  .join(", ");
912
- this.contextManager.addMessage({
913
- role: "system",
914
- content: `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`,
915
- });
890
+ this.pendingRunContext.pluginSkills = `[Plugins: ${skillSummary}] — Skills auto-activate on matching files/errors.`;
891
+ this.refreshSystemPrompt();
916
892
  }
917
- // ContinuousReflection 생성 (1분 간격 체크포인트 + 자기검증 + 컨텍스트 모니터)
918
893
  this.continuousReflection = new ContinuousReflection({
919
894
  getState: () => this.getStateSnapshot(),
920
895
  checkpoint: async (state, _emergency) => {
@@ -965,7 +940,6 @@ export class AgentLoop extends EventEmitter {
965
940
  }
966
941
  },
967
942
  });
968
- // Wire ContinuousReflection events to agent loop
969
943
  this.continuousReflection.on("reflection:feedback", (feedback) => {
970
944
  this.contextManager.addMessage({
971
945
  role: "system",
@@ -981,7 +955,6 @@ export class AgentLoop extends EventEmitter {
981
955
  this.continuousReflection.on("reflection:context_overflow", () => {
982
956
  void this.handleSoftContextOverflow();
983
957
  });
984
- // Mark fully initialized
985
958
  this.initialized = true;
986
959
  this.partialInit = false;
987
960
  }
@@ -1014,6 +987,35 @@ export class AgentLoop extends EventEmitter {
1014
987
  }
1015
988
  return parts.length > 0 ? parts.join("\n") : null;
1016
989
  }
990
+ /**
991
+ * 3-Layer Prompt 재빌드: pendingRunContext + cached state → PromptRuntime → PromptBuilder.
992
+ * backgroundInit()에서 부분 컨텍스트가 채워질 때마다 호출.
993
+ */
994
+ refreshSystemPrompt() {
995
+ if (!this.initialized && !this.partialInit)
996
+ return;
997
+ const projectPath = this.config.loop.projectPath;
998
+ const allTools = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
999
+ try {
1000
+ const envelope = compilePromptEnvelope({
1001
+ decision: this.decision,
1002
+ promptOptions: {
1003
+ projectStructure: this._cachedProjectStructure,
1004
+ yuanMdContent: this._cachedYuanMdContent,
1005
+ tools: allTools,
1006
+ activeToolNames: allTools.map(t => t.name),
1007
+ projectPath,
1008
+ environment: this.environment,
1009
+ },
1010
+ runContext: this.pendingRunContext,
1011
+ });
1012
+ const prompt = buildPrompt(envelope);
1013
+ this.contextManager.replaceSystemMessage(prompt);
1014
+ }
1015
+ catch {
1016
+ // If PromptRuntime fails, leave existing system message as-is
1017
+ }
1018
+ }
1017
1019
  /**
1018
1020
  * 에이전트 루프를 실행.
1019
1021
  * 첫 호출 시 자동으로 Memory와 프로젝트 컨텍스트를 로드한다.
@@ -1026,7 +1028,6 @@ export class AgentLoop extends EventEmitter {
1026
1028
  dlog("AGENT-LOOP", `run() called`, { goal: userMessage.slice(0, 120), resumedFromSession: this.resumedFromSession });
1027
1029
  this.reasoningAggregator.reset();
1028
1030
  this.reasoningTree.reset();
1029
- // Capture before reset so session snapshot gets accurate file list
1030
1031
  const prevChangedFiles = [...this.changedFiles];
1031
1032
  if (!this.resumedFromSession) {
1032
1033
  this.changedFiles = [];
@@ -1043,69 +1044,58 @@ export class AgentLoop extends EventEmitter {
1043
1044
  total: 0,
1044
1045
  };
1045
1046
  this.impactHintInjected = false;
1046
- // Task 3: reset tsc tracking per run
1047
+ this.pendingRunContext = {};
1048
+ this.decision = DEFAULT_DECISION;
1049
+ this.toolUsageCounter = { reads: 0, edits: 0, shells: 0, tests: 0, searches: 0, webLookups: 0, sameFileEdits: new Map() };
1047
1050
  this.iterationTsFilesModified = [];
1048
1051
  this.tscRanLastIteration = false;
1049
- // Task 1: reset context summarization guard per run
1050
1052
  this._contextSummarizationDone = false;
1051
1053
  this._unfulfilledContinuations = 0;
1052
- // aggressive recovery fields removed
1053
1054
  }
1054
- // Reset resumedFromSession flag after consuming it — prevents stale state
1055
- // from leaking into subsequent runs when the same AgentLoop instance is reused.
1056
1055
  this.resumedFromSession = false;
1057
1056
  this.checkpointSaved = false;
1058
1057
  this.failureRecovery.reset();
1058
+ this.patchJournal?.reset();
1059
+ this.patchScopeController?.reset();
1059
1060
  this.costOptimizer.reset();
1060
1061
  this.tokenBudgetManager.reset();
1061
1062
  const runStartTime = Date.now();
1062
- // 즉시 start 이벤트 emit — init 전에 TUI 타이머/상태 시작
1063
1063
  dlog("AGENT-LOOP", `emitting agent:start, sessionId=${this.sessionId}`);
1064
1064
  this.emitEvent({ kind: "agent:start", goal: userMessage });
1065
- // 첫 실행 시 메모리/프로젝트 컨텍스트 자동 로드
1066
- // criticalInit은 최대 1초만 블로킹 — LLM 호출 TTFT 최소화
1067
- // backgroundInit은 fire-and-forget — LLM 호출을 블로킹하지 않음
1068
1065
  await Promise.race([
1069
1066
  this.criticalInit(),
1070
1067
  new Promise(resolve => setTimeout(resolve, 1_000)),
1071
1068
  ]);
1072
- // If criticalInit timed out (partialInit still true), allow retry on next run
1073
1069
  if (this.partialInit && !this.initialized) {
1074
1070
  this.partialInit = false;
1075
1071
  }
1076
- // Background init — does NOT block LLM call
1077
1072
  this.backgroundInit().catch(() => { });
1078
- // Always generate a fresh sessionId per run — prevents BudgetGovernorV2
1079
- // from accumulating exhausted task budget across multiple runs on the same instance.
1080
1073
  this.sessionId = randomUUID();
1081
- // Phase 6: start task budget tracking
1074
+ // Initialize patch transaction journal for atomic rollback
1075
+ if (this.config.loop.projectPath) {
1076
+ this.patchJournal = new PatchTransactionJournal(this.config.loop.projectPath, this.sessionId);
1077
+ }
1082
1078
  try {
1083
1079
  this.budgetGovernorV2?.startTask(this.sessionId ?? "default");
1084
1080
  initMarketPlaybooks(this.strategyMarket);
1085
1081
  }
1086
1082
  catch { /* non-fatal */ }
1087
- // Initialize trace recorder (once per run)
1088
1083
  if (!this.traceRecorder) {
1089
1084
  this.traceRecorder = new TraceRecorder(this.sessionId);
1090
1085
  }
1091
- // Initialize arch summarizer (once per run)
1092
1086
  if (!this.archSummarizer && this.config.loop.projectPath) {
1093
1087
  this.archSummarizer = new ArchSummarizer(this.config.loop.projectPath);
1094
- // Trigger refresh in background if dirty
1095
1088
  this.archSummarizer.getSummary().catch(() => { });
1096
1089
  }
1097
- // Phase 4: PlaybookLibrary — inject hint into system context based on goal
1098
1090
  if (this.playbookLibrary) {
1099
1091
  try {
1100
1092
  const playbook = this.playbookLibrary.query(userMessage);
1101
1093
  if (playbook) {
1102
- this.contextManager.addMessage({
1103
- role: "system",
1104
- content: `[Playbook: ${playbook.taskType} v${playbook.version}] ` +
1094
+ this.pendingRunContext.playbookHint =
1095
+ `[Playbook: ${playbook.taskType} v${playbook.version}] ` +
1105
1096
  `Phase order: ${playbook.phaseOrder.join(" → ")}. ` +
1106
1097
  `Stop conditions: ${playbook.stopConditions.join(", ")}. ` +
1107
- `Evidence required: ${playbook.evidenceRequirements.join(", ")}.`,
1108
- });
1098
+ `Evidence required: ${playbook.evidenceRequirements.join(", ")}.`;
1109
1099
  }
1110
1100
  }
1111
1101
  catch { /* non-fatal */ }
@@ -1137,7 +1127,6 @@ export class AgentLoop extends EventEmitter {
1137
1127
  changedFiles: prevChangedFiles,
1138
1128
  });
1139
1129
  }
1140
- // 사용자 입력 검증 (prompt injection 방어)
1141
1130
  const inputValidation = this.promptDefense.validateUserInput(userMessage);
1142
1131
  if (inputValidation.injectionDetected && (inputValidation.severity === "critical" || inputValidation.severity === "high")) {
1143
1132
  this.emitEvent({
@@ -1146,18 +1135,14 @@ export class AgentLoop extends EventEmitter {
1146
1135
  retryable: false,
1147
1136
  });
1148
1137
  }
1149
- // 사용자 메시지 추가
1150
1138
  this.contextManager.addMessage({
1151
1139
  role: "user",
1152
1140
  content: userMessage,
1153
1141
  });
1154
- // PersonaManager — 유저 메시지로 커뮤니케이션 스타일 학습
1155
1142
  this.lastUserMessage = userMessage;
1156
1143
  if (this.personaManager) {
1157
1144
  this.personaManager.analyzeUserMessage(userMessage);
1158
1145
  }
1159
- // Vision Intent Detection — user message
1160
- // If the user signals they want to look at an image, auto-read it and inject as vision.
1161
1146
  try {
1162
1147
  const visionIntent = this.visionIntentDetector.detect(userMessage);
1163
1148
  if (visionIntent && visionIntent.confidence >= 0.5) {
@@ -1186,18 +1171,14 @@ export class AgentLoop extends EventEmitter {
1186
1171
  }
1187
1172
  }
1188
1173
  }
1189
- catch {
1190
- // Vision intent detection is non-fatal; continue normally
1191
- }
1174
+ catch { /* non-fatal */ }
1192
1175
  try {
1193
- // Persona injection — 유저 선호도/언어/스타일 어댑테이션을 시스템 메시지로 주입
1194
1176
  if (this.personaManager) {
1195
1177
  const personaSection = this.personaManager.buildPersonaPrompt();
1196
1178
  if (personaSection) {
1197
- this.contextManager.addMessage({ role: "system", content: personaSection });
1179
+ this.pendingRunContext.personaSection = personaSection;
1198
1180
  }
1199
1181
  }
1200
- // MemoryManager.getRelevant() — 현재 태스크와 관련된 conventions/patterns/warnings 주입
1201
1182
  if (this.memoryManager) {
1202
1183
  const relevant = this.memoryManager.getRelevant(userMessage);
1203
1184
  const parts = [];
@@ -1211,13 +1192,9 @@ export class AgentLoop extends EventEmitter {
1211
1192
  parts.push(`## Relevant Code Patterns\n${relevant.patterns.slice(0, 3).map((p) => `- **${p.name}**: ${p.description}`).join("\n")}`);
1212
1193
  }
1213
1194
  if (parts.length > 0) {
1214
- this.contextManager.addMessage({
1215
- role: "system",
1216
- content: `[Task Memory]\n${parts.join("\n\n")}`,
1217
- });
1195
+ this.pendingRunContext.taskMemory = `[Task Memory]\n${parts.join("\n\n")}`;
1218
1196
  }
1219
1197
  }
1220
- // VectorStore RAG — 태스크와 의미적으로 유사한 코드 컨텍스트 검색·주입
1221
1198
  if (this.vectorStore) {
1222
1199
  try {
1223
1200
  const hits = await this.vectorStore.search(userMessage, 3, 0.2);
@@ -1225,70 +1202,27 @@ export class AgentLoop extends EventEmitter {
1225
1202
  const ragCtx = hits
1226
1203
  .map((h) => `**${h.id}** (relevance: ${(h.similarity * 100).toFixed(0)}%)\n${h.text.slice(0, 400)}`)
1227
1204
  .join("\n\n---\n\n");
1228
- this.contextManager.addMessage({
1229
- role: "system",
1230
- content: `[RAG Context — semantically relevant code snippets]\n${ragCtx}`,
1231
- });
1205
+ this.pendingRunContext.ragContext = `[RAG Context — semantically relevant code snippets]\n${ragCtx}`;
1232
1206
  }
1233
1207
  }
1234
- catch {
1235
- // VectorStore search failure is non-fatal
1236
- }
1208
+ catch { /* non-fatal */ }
1237
1209
  }
1238
- // Reflexion: 과거 실행에서 배운 가이던스 주입
1239
1210
  if (this.reflexionEngine) {
1240
1211
  try {
1241
1212
  const guidance = await this.reflexionEngine.getGuidance(userMessage);
1242
- // 가이던스 유효성 검증: 빈 전략이나 매우 낮은 confidence 필터링
1243
1213
  const validStrategies = guidance.relevantStrategies.filter((s) => s.strategy && s.strategy.length > 5 && s.confidence > 0.1);
1244
1214
  if (validStrategies.length > 0 || guidance.recentFailures.length > 0) {
1245
1215
  const filteredGuidance = { ...guidance, relevantStrategies: validStrategies };
1246
1216
  const guidancePrompt = this.reflexionEngine.formatForSystemPrompt(filteredGuidance);
1247
- this.contextManager.addMessage({
1248
- role: "system",
1249
- content: guidancePrompt,
1250
- });
1217
+ this.pendingRunContext.reflexionGuidance = guidancePrompt;
1251
1218
  }
1252
1219
  }
1253
- catch {
1254
- // guidance 로드 실패는 치명적이지 않음
1255
- }
1256
- }
1257
- // Task 분류 → 시스템 프롬프트에 tool sequence hint 주입
1258
- const classification = this.taskClassifier.classify(userMessage);
1259
- if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1260
- const classificationHint = this.taskClassifier.formatForSystemPrompt(classification);
1261
- this.contextManager.addMessage({
1262
- role: "system",
1263
- content: classificationHint,
1264
- });
1265
- }
1266
- // Specialist routing: 태스크 타입에 맞는 전문 에이전트 설정 주입
1267
- if (classification.specialistDomain) {
1268
- const specialistMatch = this.specialistRegistry.findSpecialist(classification.specialistDomain);
1269
- if (specialistMatch && specialistMatch.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1270
- this.contextManager.addMessage({
1271
- role: "system",
1272
- content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
1273
- });
1274
- }
1275
- }
1276
- // Tool Planning: 태스크 타입에 맞는 도구 실행 계획 힌트 주입
1277
- if (this.enableToolPlanning && classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1278
- const planContext = {
1279
- userMessage,
1280
- };
1281
- this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
1282
- this.executedToolNames = [];
1283
- if (this.currentToolPlan.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1284
- const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
1285
- this.contextManager.addMessage({
1286
- role: "system",
1287
- content: planHint,
1288
- });
1289
- }
1220
+ catch { /* non-fatal */ }
1290
1221
  }
1291
- // CrossFileRefactor: detect rename/move intent and inject preview hint
1222
+ this.refreshSystemPrompt();
1223
+ // Legacy task classification block removed — Decision Engine is now always present.
1224
+ // Task classification, specialist routing, and tool planning are handled via
1225
+ // decision.core.skillActivation in the Decision-based overrides section below.
1292
1226
  if (this.config.loop.projectPath) {
1293
1227
  try {
1294
1228
  const renameMatch = userMessage.match(/\brename\s+[`'"]?(\w[\w.]*)[`'"]?\s+to\s+[`'"]?(\w[\w.]*)[`'"]/i);
@@ -1316,7 +1250,6 @@ export class AgentLoop extends EventEmitter {
1316
1250
  else if (moveMatch) {
1317
1251
  const [, symbolOrFile, destination] = moveMatch;
1318
1252
  const refactor = new CrossFileRefactor(this.config.loop.projectPath);
1319
- // Try move as symbol move (source heuristic: look for a file with that name)
1320
1253
  const preview = await refactor.moveSymbol(symbolOrFile, symbolOrFile, destination);
1321
1254
  if (preview.totalChanges > 0) {
1322
1255
  const affectedList = preview.affectedFiles
@@ -1335,13 +1268,123 @@ export class AgentLoop extends EventEmitter {
1335
1268
  }
1336
1269
  }
1337
1270
  }
1338
- catch {
1339
- // CrossFileRefactor preview failure is non-fatal
1271
+ catch { /* non-fatal */ }
1272
+ }
1273
+ try {
1274
+ let projectCtx = this.worldState
1275
+ ? worldStateToProjectContext(this.worldState)
1276
+ : undefined;
1277
+ // Codebase complexity stats are enriched by ExecutionEngine when it calls agentDecide
1278
+ // AgentLoop uses worldState only; CodebaseContext lives in ExecutionEngine
1279
+ this.decision = agentDecide({
1280
+ message: userMessage,
1281
+ projectContext: projectCtx,
1282
+ prevDecision: this.prevDecision ?? undefined,
1283
+ });
1284
+ this.emitEvent({
1285
+ kind: "agent:decision",
1286
+ decision: {
1287
+ intent: this.decision.core.reasoning.intent,
1288
+ complexity: this.decision.core.reasoning.complexity,
1289
+ taskStage: this.decision.core.reasoning.taskStage,
1290
+ planRequired: this.decision.core.planRequired,
1291
+ nextAction: this.decision.core.nextAction,
1292
+ },
1293
+ });
1294
+ this.emitEvent({
1295
+ kind: "agent:interaction_mode",
1296
+ mode: this.decision.core.interactionMode,
1297
+ });
1298
+ if (this.decision.core.nextAction === "ask_user" && this.decision.core.clarification) {
1299
+ const clar = this.decision.core.clarification;
1300
+ const clarMsg = `I need some clarification before proceeding:\n\n${clar.reason}` +
1301
+ (clar.missingFields.length > 0 ? `\n\nMissing: ${clar.missingFields.join(", ")}` : "") +
1302
+ (clar.allowProceedWithAssumptions ? "\n\n(I can proceed with assumptions if you prefer.)" : "");
1303
+ this.emitEvent({ kind: "agent:completed", summary: clarMsg, filesChanged: [] });
1304
+ return { reason: "NEEDS_CLARIFICATION", summary: clarMsg };
1305
+ }
1306
+ if (this.decision.core.nextAction === "blocked_external") {
1307
+ const blockedMsg = "This task appears to be blocked by an external dependency. Please resolve the blocking issue and retry.";
1308
+ this.emitEvent({ kind: "agent:completed", summary: blockedMsg, filesChanged: [] });
1309
+ return { reason: "BLOCKED_EXTERNAL", summary: blockedMsg };
1310
+ }
1311
+ this.currentComplexity = this.decision.core.reasoning.complexity;
1312
+ const decMode = this.decision.core.interactionMode;
1313
+ const decVd = this.decision.core.verifyDepth;
1314
+ if (decMode === "CHAT") {
1315
+ this.overheadGovernor.overrideConfig({
1316
+ autoTsc: "OFF",
1317
+ debate: "OFF",
1318
+ deepVerify: "OFF",
1319
+ quickVerify: "OFF",
1320
+ qaPipeline: "OFF",
1321
+ llmFixer: "OFF",
1322
+ summarize: "OFF",
1323
+ });
1324
+ }
1325
+ else if (decMode === "AGENT" && decVd === "thorough") {
1326
+ this.overheadGovernor.overrideConfig({
1327
+ autoTsc: "BLOCKING",
1328
+ deepVerify: "BLOCKING",
1329
+ quickVerify: "BLOCKING",
1330
+ qaPipeline: "BLOCKING",
1331
+ });
1332
+ }
1333
+ else if (decMode === "HYBRID") {
1334
+ this.overheadGovernor.overrideConfig({
1335
+ autoTsc: "SHADOW",
1336
+ quickVerify: "SHADOW",
1337
+ qaPipeline: "SHADOW",
1338
+ });
1339
+ }
1340
+ // ── Initialize safety floor modules (MutationPolicy + PatchScopeController) ──
1341
+ if (this.config.loop.projectPath) {
1342
+ this.mutationPolicy = new WorkspaceMutationPolicy(this.config.loop.projectPath);
1343
+ const lifecycle = detectRepoLifecycle(this.config.loop.projectPath);
1344
+ this.patchScopeController = new PatchScopeController(this.decision.core.reasoning.complexity, lifecycle);
1345
+ }
1346
+ // ── Decision-based memory load override (Phase I+ SSOT) ──
1347
+ // If Decision says memory is not needed, clear already-loaded memory context
1348
+ // to reduce prompt size for trivial tasks.
1349
+ const shouldLoadMemory = this.decision.core.memoryLoad.shouldLoad;
1350
+ if (!shouldLoadMemory) {
1351
+ this.pendingRunContext.memoryContext = undefined;
1352
+ this.pendingRunContext.taskMemory = undefined;
1353
+ this.pendingRunContext.ragContext = undefined;
1354
+ }
1355
+ // ── Decision-based skill/specialist/background overrides (Phase I SSOT) ──
1356
+ const sa = this.decision.core.skillActivation;
1357
+ // Specialist routing from Decision
1358
+ if (sa.enableSpecialist && sa.specialistDomain) {
1359
+ const specialistMatch = this.specialistRegistry.findSpecialist(sa.specialistDomain);
1360
+ if (specialistMatch && specialistMatch.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1361
+ this.contextManager.addMessage({
1362
+ role: "system",
1363
+ content: `[Specialist: ${specialistMatch.specialist.name}] ${specialistMatch.specialist.systemPrompt.slice(0, 500)}`,
1364
+ });
1365
+ }
1340
1366
  }
1367
+ // Tool planning from Decision
1368
+ if (sa.enableToolPlanning) {
1369
+ const classification = this.taskClassifier.classify(userMessage);
1370
+ if (classification.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1371
+ const planContext = { userMessage };
1372
+ this.currentToolPlan = this.toolPlanner.planForTask(classification.type, planContext);
1373
+ this.executedToolNames = [];
1374
+ if (this.currentToolPlan.confidence >= CLASSIFICATION_CONFIDENCE_THRESHOLD) {
1375
+ const planHint = this.toolPlanner.formatPlanHint(this.currentToolPlan);
1376
+ this.contextManager.addMessage({
1377
+ role: "system",
1378
+ content: planHint,
1379
+ });
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
+ catch {
1385
+ this.decision = DEFAULT_DECISION;
1341
1386
  }
1342
- // 복잡도 감지 → 필요 시 자동 플래닝
1343
1387
  await this.maybeCreatePlan(userMessage);
1344
- // ContinuousReflection 시작 (1분 간격 체크포인트/자기검증/컨텍스트모니터)
1345
1388
  if (this.continuousReflection) {
1346
1389
  this.continuousReflection.start();
1347
1390
  }
@@ -1350,20 +1393,19 @@ export class AgentLoop extends EventEmitter {
1350
1393
  result = await this.executeLoop();
1351
1394
  }
1352
1395
  finally {
1353
- // ContinuousReflection 정지 (루프 종료 시 반드시 정리)
1354
1396
  if (this.continuousReflection) {
1355
1397
  this.continuousReflection.stop();
1356
1398
  }
1357
1399
  }
1400
+ this.prevDecision = this.decision;
1401
+ this.toolUsageCounter = { reads: 0, edits: 0, shells: 0, tests: 0, searches: 0, webLookups: 0, sameFileEdits: new Map() };
1358
1402
  if (this.sessionPersistence && this.sessionId) {
1359
1403
  const finalStatus = result.reason === "ERROR"
1360
1404
  ? "crashed"
1361
1405
  : "completed";
1362
1406
  await this.sessionPersistence.updateStatus(this.sessionId, finalStatus);
1363
1407
  }
1364
- // 실행 완료 후 메모리 자동 업데이트
1365
1408
  await this.updateMemoryAfterRun(userMessage, result, Date.now() - runStartTime);
1366
- // Phase 4: FailureSignatureMemory promote + PlaybookLibrary outcome recording
1367
1409
  if (result.reason === "GOAL_ACHIEVED") {
1368
1410
  try {
1369
1411
  if (this.failureSigMemory && this.repeatedErrorSignature) {
@@ -1378,7 +1420,6 @@ export class AgentLoop extends EventEmitter {
1378
1420
  }
1379
1421
  catch { /* non-fatal */ }
1380
1422
  }
1381
- // Phase 4 remaining: SelfImprovementLoop + MetaLearningCollector + TrustEconomics
1382
1423
  try {
1383
1424
  const taskType = this.playbookLibrary?.query(userMessage)?.taskType ?? "unknown";
1384
1425
  const championPlaybook = selectMarketStrategy(this.strategyMarket, taskType);
@@ -1419,7 +1460,6 @@ export class AgentLoop extends EventEmitter {
1419
1460
  this.trustEconomics.record(ac, tr.success);
1420
1461
  }
1421
1462
  }
1422
- // Phase 5: record strategy outcome
1423
1463
  if (this.strategyLearner) {
1424
1464
  const activePlaybookId = this._activePlaybookId ?? "default";
1425
1465
  if (runSuccess) {
@@ -1429,7 +1469,6 @@ export class AgentLoop extends EventEmitter {
1429
1469
  this.strategyLearner.recordFailure(activePlaybookId, taskType, this.iterationCount, this.tokenUsage.total);
1430
1470
  }
1431
1471
  }
1432
- // Phase 5 extended: periodic pattern extraction (every 10 runs)
1433
1472
  this.sessionRunCount += 1;
1434
1473
  if (this.sessionRunCount % 10 === 0) {
1435
1474
  if (this.tracePatternExtractor) {
@@ -1439,7 +1478,6 @@ export class AgentLoop extends EventEmitter {
1439
1478
  this.metaLearningEngine.analyze().catch(() => { });
1440
1479
  }
1441
1480
  }
1442
- // Phase 6: record outcomes in capability self model and strategy market
1443
1481
  try {
1444
1482
  const env = this._detectedEnvironment ?? "general";
1445
1483
  this.capabilitySelfModel?.recordOutcome(env, taskType, runSuccess);
@@ -1453,13 +1491,11 @@ export class AgentLoop extends EventEmitter {
1453
1491
  catch { /* non-fatal */ }
1454
1492
  }
1455
1493
  catch { /* non-fatal */ }
1456
- // SkillLearner: 성공적 에러 해결 시 새로운 스킬 학습
1457
1494
  if (this.skillLearner && result.reason === "GOAL_ACHIEVED") {
1458
1495
  try {
1459
1496
  let newSkillId = null;
1460
1497
  const errorToolResults = this.allToolResults.filter((r) => !r.success);
1461
1498
  if (errorToolResults.length > 0 && this.changedFiles.length > 0) {
1462
- // 에러가 있었지만 결국 성공 → 학습 가능한 패턴
1463
1499
  const runAnalysis = this.memoryUpdater.analyzeRun({
1464
1500
  goal: userMessage,
1465
1501
  termination: { reason: result.reason, summary: result.summary },
@@ -1487,27 +1523,17 @@ export class AgentLoop extends EventEmitter {
1487
1523
  }
1488
1524
  }
1489
1525
  }
1490
- catch {
1491
- // 학습 실패는 치명적이지 않음
1492
- }
1526
+ catch { /* non-fatal */ }
1493
1527
  }
1494
- // Tool plan compliance check (non-blocking, for metrics)
1495
1528
  if (this.currentToolPlan && this.executedToolNames.length > 0) {
1496
1529
  try {
1497
1530
  this.toolPlanner.validateExecution(this.currentToolPlan, this.executedToolNames);
1498
1531
  }
1499
- catch {
1500
- // compliance check 실패는 치명적이지 않음
1501
- }
1532
+ catch { /* non-fatal */ }
1502
1533
  }
1503
- // RepoKnowledgeGraph: 변경 파일 그래프 업데이트
1504
1534
  if (this.repoGraph && this.changedFiles.length > 0) {
1505
- this.repoGraph.updateFiles(this.changedFiles).catch(() => {
1506
- // 그래프 업데이트 실패는 치명적이지 않음
1507
- });
1535
+ this.repoGraph.updateFiles(this.changedFiles).catch(() => { });
1508
1536
  }
1509
- // BackgroundAgentManager: 정리 (stopAll은 abort 시에만)
1510
- // Background agents는 세션 간 지속되므로 여기서 stop하지 않음
1511
1537
  return result;
1512
1538
  }
1513
1539
  catch (err) {
@@ -1532,10 +1558,10 @@ export class AgentLoop extends EventEmitter {
1532
1558
  * - 성공/실패 패턴 학습
1533
1559
  */
1534
1560
  async updateMemoryAfterRun(userGoal, result, runDurationMs = 0) {
1535
- if (!this.enableMemory || !this.memoryManager)
1561
+ // Gate by both memoryLoad (loading) and memoryIntent (saving)
1562
+ if (!this.decision.core.memoryIntent.shouldSave || !this.memoryManager)
1536
1563
  return;
1537
1564
  try {
1538
- // MemoryUpdater로 풍부한 학습 추출
1539
1565
  const analysis = this.memoryUpdater.analyzeRun({
1540
1566
  goal: userGoal,
1541
1567
  termination: {
@@ -1558,16 +1584,13 @@ export class AgentLoop extends EventEmitter {
1558
1584
  durationMs: runDurationMs,
1559
1585
  iterations: this.iterationCount,
1560
1586
  });
1561
- // 추출된 학습을 MemoryManager에 저장
1562
1587
  const learnings = this.memoryUpdater.extractLearnings(analysis, userGoal);
1563
1588
  for (const learning of learnings) {
1564
1589
  this.memoryManager.addLearning(learning.category, learning.content);
1565
1590
  }
1566
- // 감지된 컨벤션 저장 (기존에 추출만 되고 저장 안 되던 버그 수정)
1567
1591
  for (const convention of analysis.conventions) {
1568
1592
  this.memoryManager.addConvention(convention);
1569
1593
  }
1570
- // 감지된 패턴 저장
1571
1594
  for (const pattern of analysis.toolPatterns) {
1572
1595
  if (pattern.successRate > 0.7 && pattern.count >= 3) {
1573
1596
  this.memoryManager.addPattern({
@@ -1578,25 +1601,18 @@ export class AgentLoop extends EventEmitter {
1578
1601
  });
1579
1602
  }
1580
1603
  }
1581
- // 에러로 종료된 경우 실패 기록도 추가
1582
1604
  if (result.reason === "ERROR") {
1583
1605
  this.memoryManager.addFailedApproach(`Task: ${userGoal.slice(0, 80)}`, result.error ?? "Unknown error");
1584
1606
  }
1585
- // 오래된 항목 정리 (매 5회 실행마다)
1586
1607
  if (this.iterationCount % 5 === 0) {
1587
1608
  this.memoryManager.prune();
1588
1609
  }
1589
- // 메모리 저장
1590
1610
  await this.memoryManager.save();
1591
- // PersonaManager — 유저 프로필 저장 (학습된 커뮤니케이션 스타일 유지)
1592
1611
  if (this.personaManager) {
1593
1612
  await this.personaManager.saveProfile().catch(() => { });
1594
1613
  }
1595
1614
  }
1596
- catch {
1597
- // 메모리 저장 실패는 치명적이지 않음
1598
- }
1599
- // Reflexion: 실행 결과 반영 + 전략 추출
1615
+ catch { /* non-fatal */ }
1600
1616
  if (this.reflexionEngine) {
1601
1617
  try {
1602
1618
  const entry = this.reflexionEngine.reflect({
@@ -1610,7 +1626,6 @@ export class AgentLoop extends EventEmitter {
1610
1626
  changedFiles: this.changedFiles,
1611
1627
  });
1612
1628
  await this.reflexionEngine.store.saveReflection(entry);
1613
- // 성공 시 전략 추출
1614
1629
  if (entry.outcome === "success") {
1615
1630
  const strategy = this.reflexionEngine.extractStrategy(entry, userGoal);
1616
1631
  if (strategy) {
@@ -1618,9 +1633,7 @@ export class AgentLoop extends EventEmitter {
1618
1633
  }
1619
1634
  }
1620
1635
  }
1621
- catch {
1622
- // reflexion 저장 실패는 치명적이지 않음
1623
- }
1636
+ catch { /* non-fatal */ }
1624
1637
  }
1625
1638
  }
1626
1639
  /**
@@ -1747,11 +1760,28 @@ export class AgentLoop extends EventEmitter {
1747
1760
  lines.push(`changed: ${files}`);
1748
1761
  }
1749
1762
  lines.push(`token_budget: ${remaining}% remaining`);
1750
- if (this.iterationSystemMsgCount < 5) {
1751
- this.contextManager.addMessage({ role: "system", content: lines.join(" | ") });
1752
- this.lastAgentStateInjection = iteration;
1753
- this.iterationSystemMsgCount++;
1763
+ this.ephemeralHints.push(lines.join(" | "));
1764
+ this.lastAgentStateInjection = iteration;
1765
+ }
1766
+ /**
1767
+ * Phase B: flush collected ephemeral hints into context as system messages.
1768
+ * Applies rate-limiting: max 7 hints, max 3000 tokens total (matching PromptRuntime compileEphemeral).
1769
+ * Called at end of each iteration (before next LLM call) and before `continue` statements.
1770
+ */
1771
+ flushEphemeralHints() {
1772
+ if (this.ephemeralHints.length === 0)
1773
+ return;
1774
+ const MAX_HINTS = 7;
1775
+ const MAX_TOKENS = 3000;
1776
+ let tokenCount = 0;
1777
+ for (const hint of this.ephemeralHints.slice(0, MAX_HINTS)) {
1778
+ const tokens = Math.ceil(hint.length / 3.5);
1779
+ if (tokenCount + tokens > MAX_TOKENS)
1780
+ break;
1781
+ tokenCount += tokens;
1782
+ this.contextManager.addMessage({ role: "system", content: hint });
1754
1783
  }
1784
+ this.ephemeralHints = [];
1755
1785
  }
1756
1786
  /**
1757
1787
  * LLM 응답 텍스트에서 "Updated hypothesis:" 마커를 파싱해 hypothesis를 갱신한다.
@@ -1816,24 +1846,11 @@ export class AgentLoop extends EventEmitter {
1816
1846
  * - "moderate" 이상 → HierarchicalPlanner로 L1+L2 계획 수립
1817
1847
  */
1818
1848
  async maybeCreatePlan(userMessage) {
1819
- if (!this.planner || !this.enablePlanning)
1849
+ if (!this.planner)
1820
1850
  return;
1821
- const complexity = await this.detectComplexity(userMessage);
1822
- this.currentComplexity = complexity;
1823
- // 임계값 미만이면 플래닝 스킵
1824
- // Bug 4 fix: extend thresholdOrder to include "massive" (4), so that when planningThreshold
1825
- // is "complex", both "complex" (3) and "massive" (4) trigger planning.
1826
- // Previously "massive" had no entry and fell through to undefined → NaN comparisons.
1827
- const thresholdOrder = { simple: 1, moderate: 2, complex: 3, massive: 4 };
1828
- const complexityOrder = {
1829
- trivial: 0, simple: 1, moderate: 2, complex: 3, massive: 4,
1830
- };
1831
- // Use the threshold for the configured level; "complex" threshold activates for complexity >= "complex"
1832
- const effectiveThreshold = thresholdOrder[this.planningThreshold] ?? 2;
1833
- if ((complexityOrder[complexity] ?? 0) < effectiveThreshold) {
1851
+ if (!this.decision.core.planRequired)
1834
1852
  return;
1835
- }
1836
- this.emitSubagent("planner", "start", `task complexity ${complexity}. creating execution plan`);
1853
+ this.emitSubagent("planner", "start", `task complexity ${this.currentComplexity}. creating execution plan`);
1837
1854
  try {
1838
1855
  const plan = await this.planner.createHierarchicalPlan(userMessage, this.llmClient);
1839
1856
  this.activePlan = plan;
@@ -1869,13 +1886,8 @@ export class AgentLoop extends EventEmitter {
1869
1886
  });
1870
1887
  }
1871
1888
  }
1872
- /**
1873
- * 사용자 메시지에서 태스크 복잡도를 휴리스틱으로 추정.
1874
- * LLM 호출 없이 빠르게 결정 (토큰 절약).
1875
- */
1876
1889
  /**
1877
1890
  * Detect the best test/verify command for the current project.
1878
- * Bug 3 fix: replaces the hardcoded "pnpm build" default.
1879
1891
  */
1880
1892
  detectTestCommand() {
1881
1893
  const projectPath = this.config.loop.projectPath;
@@ -1907,6 +1919,7 @@ export class AgentLoop extends EventEmitter {
1907
1919
  }
1908
1920
  return "pnpm build";
1909
1921
  }
1922
+ /** @deprecated Use AgentDecisionContext.core.reasoning.complexity instead */
1910
1923
  _detectComplexityHeuristic(message) {
1911
1924
  const lower = message.toLowerCase();
1912
1925
  const len = message.length;
@@ -2011,10 +2024,9 @@ export class AgentLoop extends EventEmitter {
2011
2024
  return "massive";
2012
2025
  }
2013
2026
  /**
2014
- * Hybrid complexity detection: keyword heuristic for clear cases,
2015
- * LLM single-word classification for ambiguous borderline cases (score 1-3).
2016
- * This prevents both over-planning (trivial tasks) and under-planning
2017
- * (ambitious short requests that keywords miss across any language).
2027
+ * @deprecated Use AgentDecisionContext.core.reasoning.complexity instead.
2028
+ * Hybrid complexity detection: keyword heuristic + LLM fallback for borderline cases.
2029
+ * Only called when Decision Engine is absent.
2018
2030
  */
2019
2031
  async detectComplexity(message) {
2020
2032
  const heuristic = this._detectComplexityHeuristic(message);
@@ -2117,11 +2129,7 @@ export class AgentLoop extends EventEmitter {
2117
2129
  this.activePlan.tactical.push(modTask);
2118
2130
  }
2119
2131
  }
2120
- // 리플래닝 결과를 컨텍스트에 주입
2121
- this.contextManager.addMessage({
2122
- role: "system",
2123
- content: `[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`,
2124
- });
2132
+ this.ephemeralHints.push(`[Re-plan] Strategy: ${result.strategy}. Reason: ${result.reason}.\nModified tasks: ${result.modifiedTasks.map((t) => t.description).join(", ")}`);
2125
2133
  }
2126
2134
  // Estimate replan token usage
2127
2135
  this.tokenBudgetManager.recordUsage("planner", 500, 500);
@@ -2178,22 +2186,18 @@ export class AgentLoop extends EventEmitter {
2178
2186
  this.iterationCount = iteration;
2179
2187
  dlog("AGENT-LOOP", `── iteration ${iteration} start ──`, { tokenUsageTotal: this.tokenUsage.total });
2180
2188
  const iterationStart = Date.now();
2181
- // Phase 6: Budget governor halt check
2182
2189
  dlog("AGENT-LOOP", `BUDGET_EXHAUSTED check`, { used: this.tokenUsage.total, budget: this.config.loop.totalTokenBudget });
2183
2190
  if (checkBudgetShouldHalt(this.budgetGovernorV2, this.sessionId ?? "default")) {
2184
2191
  return { reason: "BUDGET_EXHAUSTED", tokensUsed: this.tokenUsage.total };
2185
2192
  }
2186
- // Reset per-iteration governor state
2187
2193
  this.verifyRanThisIteration = false;
2188
2194
  this.summarizeRanThisIteration = false;
2189
2195
  this.llmFixerRunCount = 0;
2190
2196
  this.iterationSystemMsgCount = 0;
2191
- // Message pruning — cap in-memory messages to prevent OOM (GPT recommendation)
2197
+ this.ephemeralHints = [];
2192
2198
  this.pruneMessagesIfNeeded();
2193
- // Cap allToolResults (prevents unbounded memory growth)
2194
2199
  this.allToolResults = cap(this.allToolResults, BOUNDS.allToolResults);
2195
2200
  this.allToolResultsSinceLastReplan = cap(this.allToolResultsSinceLastReplan, BOUNDS.toolResultsSinceReplan);
2196
- // Proactive replanning check (every 10 iterations when plan is active)
2197
2201
  if (this.replanningEngine &&
2198
2202
  this.activePlan &&
2199
2203
  this.activeMilestones.length > 0 &&
@@ -2217,38 +2221,21 @@ export class AgentLoop extends EventEmitter {
2217
2221
  this.activePlan.tactical[idx] = modified;
2218
2222
  }
2219
2223
  }
2220
- // Reset tool results accumulator after replan
2221
2224
  this.allToolResultsSinceLastReplan = [];
2222
- // Inject replan notice into context
2223
- this.contextManager.addMessage({
2224
- role: "system",
2225
- content: `[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`,
2226
- });
2225
+ this.ephemeralHints.push(`[Proactive Replan] ${replanResult.message}\nScope: ${replanResult.decision.scope}, Risk: ${replanResult.decision.urgency}`);
2227
2226
  }
2228
2227
  }
2229
- catch {
2230
- // Non-blocking — proactive replanning failures should not crash the agent
2231
- }
2228
+ catch { /* non-fatal */ }
2232
2229
  }
2233
- // Plan progress injection — every 3 iterations or when task advances
2234
2230
  if (this.activePlan) {
2235
2231
  this.injectPlanProgress(iteration);
2236
2232
  }
2237
- // NOTE: ExecutionPolicyEngine cost.maxTokensPerIteration check was removed.
2238
- // That check compared cumulative token totals against a per-iteration limit (50k default),
2239
- // causing premature BUDGET_EXHAUSTED at ~50k total tokens regardless of configured totalTokenBudget.
2240
- // Budget enforcement is correctly handled by BudgetGovernorV2 (taskBudget: 200k) above
2241
- // and the totalTokenBudget check at the end of each iteration.
2242
- // Soft context rollover:
2243
- // checkpoint first, then let ContextManager compact instead of aborting/throwing.
2244
2233
  const contextUsageRatio = this.contextManager.getUsageRatio();
2245
- // Task 1: ContextBudgetManager LLM summarization — Governor gated (default SHADOW)
2246
2234
  const summarizeMode = this.overheadGovernor.shouldRunSummarize(this.buildTriggerContext());
2247
2235
  if (summarizeMode === "BLOCKING")
2248
2236
  this.summarizeRanThisIteration = true;
2249
2237
  if (contextUsageRatio >= 0.75 && this.contextBudgetManager && !this._contextSummarizationDone && summarizeMode === "BLOCKING") {
2250
- this._contextSummarizationDone = true; // run at most once per agent turn
2251
- // Non-blocking: fire-and-forget so the main iteration is not stalled
2238
+ this._contextSummarizationDone = true;
2252
2239
  this.contextBudgetManager.importMessages(this.contextManager.getMessages());
2253
2240
  if (this.contextBudgetManager.needsSummarization()) {
2254
2241
  const budgetMgr = this.contextBudgetManager;
@@ -2263,13 +2250,9 @@ export class AgentLoop extends EventEmitter {
2263
2250
  content: `Context at ${Math.round(ratio * 100)}%: summarized ${summary.originalIds.length} old messages (${summary.originalTokens} → ${summary.summarizedTokens} tokens, ${Math.round(summary.compressionRatio * 100)}% ratio).`,
2264
2251
  });
2265
2252
  }
2266
- }).catch(() => {
2267
- // summarization failure is non-fatal
2268
- });
2253
+ }).catch(() => { });
2269
2254
  }
2270
2255
  }
2271
- // ContextCompressor block removed: prepareForLLM() already handles compression internally.
2272
- // Double compression (ContextCompressor + prepareForLLM) caused redundant message eviction.
2273
2256
  if (contextUsageRatio >= 0.85) {
2274
2257
  if (!this.checkpointSaved) {
2275
2258
  await this.saveAutoCheckpoint(iteration);
@@ -2281,9 +2264,6 @@ export class AgentLoop extends EventEmitter {
2281
2264
  `Compressing conversation state and continuing.`,
2282
2265
  });
2283
2266
  }
2284
- // Check file-pattern skill triggers based on changed files
2285
- // Guard: skip skill injection if over 80% token budget to preserve remaining budget
2286
- // Guard: max 3 active skills globally (Context Budget Rules)
2287
2267
  const fileTriggerBudgetRatio = this.config.loop.totalTokenBudget > 0
2288
2268
  ? this.tokenUsage.total / this.config.loop.totalTokenBudget
2289
2269
  : 0;
@@ -2294,19 +2274,14 @@ export class AgentLoop extends EventEmitter {
2294
2274
  const fileSkills = this.pluginRegistry.findMatchingSkills({
2295
2275
  filePath: lastFile,
2296
2276
  });
2297
- // Max 2 skills per iteration, capped by global 3-skill limit
2298
2277
  const slotsRemaining = AgentLoop.MAX_ACTIVE_SKILLS - this.activeSkillIds.length;
2299
2278
  for (const skill of fileSkills.slice(0, Math.min(2, slotsRemaining))) {
2300
2279
  if (this.activeSkillIds.includes(skill.id))
2301
2280
  continue; // no duplicate
2302
2281
  const parsed = this.skillLoader.loadTemplate(skill);
2303
2282
  if (parsed) {
2304
- this.contextManager.addMessage({
2305
- role: "system",
2306
- content: `[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`,
2307
- });
2283
+ this.ephemeralHints.push(`[File Skill: ${skill.name}] ${parsed.domain ? `[${parsed.domain}] ` : ""}${skill.description}`);
2308
2284
  this.activeSkillIds.push(skill.id);
2309
- this.iterationSystemMsgCount++;
2310
2285
  }
2311
2286
  }
2312
2287
  }
@@ -2468,10 +2443,9 @@ export class AgentLoop extends EventEmitter {
2468
2443
  role: "assistant",
2469
2444
  content: content || "",
2470
2445
  });
2471
- this.contextManager.addMessage({
2472
- role: "system",
2473
- content: `[Self-Reflection L2] Verification failed (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence.toFixed(2)}). ${deepResult.selfCritique}${issuesList ? ` Issues: ${issuesList}` : ""}. Please address these before completing.`,
2474
- });
2446
+ this.ephemeralHints.push(`[Self-Reflection L2] Verification failed (score: ${deepResult.overallScore}, confidence: ${deepResult.confidence.toFixed(2)}). ${deepResult.selfCritique}${issuesList ? ` Issues: ${issuesList}` : ""}. Please address these before completing.`);
2447
+ // Flush ephemeral hints before continue (next iteration resets them)
2448
+ this.flushEphemeralHints();
2475
2449
  this.emitSubagent("verifier", "done", `deep verification failed, score ${deepResult.overallScore}. continuing to address issues`);
2476
2450
  continue; // Don't return GOAL_ACHIEVED, continue the loop
2477
2451
  }
@@ -2500,10 +2474,9 @@ export class AgentLoop extends EventEmitter {
2500
2474
  role: "assistant",
2501
2475
  content: content || "",
2502
2476
  });
2503
- this.contextManager.addMessage({
2504
- role: "system",
2505
- content: `[Debate] Multi-agent debate did not pass (score: ${debateResult.finalScore}). ${debateResult.summary}. Please address the identified issues.`,
2506
- });
2477
+ this.ephemeralHints.push(`[Debate] Multi-agent debate did not pass (score: ${debateResult.finalScore}). ${debateResult.summary}. Please address the identified issues.`);
2478
+ // Flush ephemeral hints before continue
2479
+ this.flushEphemeralHints();
2507
2480
  this.emitEvent({
2508
2481
  kind: "agent:thinking",
2509
2482
  content: `Debate failed (score: ${debateResult.finalScore}). Continuing to address issues...`,
@@ -2536,7 +2509,6 @@ export class AgentLoop extends EventEmitter {
2536
2509
  content: finalSummary,
2537
2510
  });
2538
2511
  }
2539
- // Task 2: QAPipeline "thorough" mode at final task completion (LLM review included)
2540
2512
  if (this.changedFiles.length > 0 && this.config.loop.projectPath) {
2541
2513
  try {
2542
2514
  const thoroughQA = new QAPipeline({
@@ -2600,18 +2572,23 @@ export class AgentLoop extends EventEmitter {
2600
2572
  if (taskCompleteCall) {
2601
2573
  const callArgs = this.parseToolArgs(taskCompleteCall.arguments);
2602
2574
  const taskCompleteSummary = String(callArgs["summary"] ?? response.content ?? "Task completed.");
2603
- // Removed stale GOAL_ACHIEVED prevention — was causing recovery loops.
2604
- // Model decides whether to retry on its own.
2605
- // Save assistant message to context filter task_complete from tool_calls.
2606
- // task_complete is an internal protocol signal; including it in LLM history without
2607
- // a matching role:"tool" result causes API 400 on the next turn.
2575
+ if (this.decision.core.vetoFlags.verifyRequired && !this.verifyRanThisIteration) {
2576
+ this.ephemeralHints.push("[VERIFY REQUIRED] You must run build/test verification before completing. Run tsc, tests, or verification commands now.");
2577
+ // Don't return completion continue loop so LLM runs verification first
2578
+ this.contextManager.addMessage({
2579
+ role: "assistant",
2580
+ content: response.content,
2581
+ tool_calls: response.toolCalls.filter((tc) => tc.name !== "task_complete"),
2582
+ });
2583
+ continue;
2584
+ }
2585
+ // Filter task_complete from tool_calls — internal protocol signal without matching tool result
2608
2586
  const nonProtocolCalls = response.toolCalls.filter((tc) => tc.name !== "task_complete");
2609
2587
  this.contextManager.addMessage({
2610
2588
  role: "assistant",
2611
2589
  content: response.content,
2612
2590
  tool_calls: nonProtocolCalls.length > 0 ? nonProtocolCalls : undefined,
2613
2591
  });
2614
- // FIX: Emit tool_result so TUI marks task_complete as "success" (stops BlinkingDot + LiveElapsed)
2615
2592
  this.emitEvent({
2616
2593
  kind: "agent:tool_result",
2617
2594
  tool: "task_complete",
@@ -2649,18 +2626,12 @@ export class AgentLoop extends EventEmitter {
2649
2626
  }
2650
2627
  // 4. 도구 실행
2651
2628
  const { results: toolResults, deferredFixPrompts } = await this.executeTools(response.toolCalls);
2652
- // Reflexion: 도구 결과 수집
2653
2629
  this.allToolResults.push(...toolResults);
2654
2630
  const failedResults = toolResults.filter(r => !r.success);
2655
- // Tool failures are already in tool result messages — LLM sees them naturally.
2656
- // Removed forced retry logic that caused recovery loops.
2657
- // Phase 6: Record tool outcomes in capability graph
2658
2631
  for (const tr of toolResults) {
2659
2632
  recordToolOutcomeInGraph(this.capabilityGraph, tr.name, tr.success);
2660
2633
  }
2661
- // Accumulate tool results for proactive replanning evaluation
2662
2634
  this.allToolResultsSinceLastReplan.push(...toolResults);
2663
- // Tool plan tracking: 실행된 도구 이름 기록
2664
2635
  for (const result of toolResults) {
2665
2636
  this.executedToolNames.push(result.name);
2666
2637
  }
@@ -2718,7 +2689,6 @@ export class AgentLoop extends EventEmitter {
2718
2689
  content: fixPrompt,
2719
2690
  });
2720
2691
  }
2721
- // Task 2: QAPipeline — run "quick" (structural only) after any WRITE tool call this iteration
2722
2692
  const projectPath = this.config.loop.projectPath;
2723
2693
  const qaMode = this.overheadGovernor.shouldRunQaPipeline(this.buildTriggerContext());
2724
2694
  if (this.iterationWriteToolPaths.length > 0 && projectPath && qaMode !== "OFF") {
@@ -2749,25 +2719,19 @@ export class AgentLoop extends EventEmitter {
2749
2719
  issues: qaIssues,
2750
2720
  });
2751
2721
  // Only inject into LLM context in BLOCKING mode (SHADOW = observe only)
2752
- if (qaMode === "BLOCKING" && failedChecks.length > 0 && this.iterationSystemMsgCount < 5) {
2722
+ if (qaMode === "BLOCKING" && failedChecks.length > 0) {
2753
2723
  const checkSummary = failedChecks
2754
2724
  .slice(0, 5)
2755
2725
  .map((c) => ` - [${c.severity}] ${c.name}: ${c.message}`)
2756
2726
  .join("\n");
2757
- this.contextManager.addMessage({
2758
- role: "system",
2759
- content: `[QA Quick Check] ${failedChecks.length} issue(s) detected in modified files:\n${checkSummary}`,
2760
- });
2761
- this.iterationSystemMsgCount++;
2727
+ this.ephemeralHints.push(`[QA Quick Check] ${failedChecks.length} issue(s) detected in modified files:\n${checkSummary}`);
2762
2728
  }
2763
2729
  }
2764
2730
  catch {
2765
2731
  // QAPipeline failure is non-fatal
2766
2732
  }
2767
2733
  }
2768
- // Reset per-iteration write tool tracking
2769
2734
  this.iterationWriteToolPaths = [];
2770
- // Phase 4: StallDetector check (non-blocking observer)
2771
2735
  if (this.stallDetector) {
2772
2736
  try {
2773
2737
  const stallResult = this.stallDetector.check(this.iterationCount, this.changedFiles, this.repeatedErrorSignature);
@@ -2784,8 +2748,6 @@ export class AgentLoop extends EventEmitter {
2784
2748
  }
2785
2749
  catch { /* non-fatal */ }
2786
2750
  }
2787
- // Task 3: Auto-run tsc --noEmit after 2+ TS files modified in this iteration
2788
- // Skip if tsc was already run in the previous iteration (cooldown)
2789
2751
  const tscFilesThisIteration = [...this.iterationTsFilesModified];
2790
2752
  this.iterationTsFilesModified = []; // reset for next iteration
2791
2753
  const tscRanPrev = this.tscRanLastIteration;
@@ -2813,15 +2775,11 @@ export class AgentLoop extends EventEmitter {
2813
2775
  content: `Auto-TSC: TypeScript errors found after editing ${tscFilesThisIteration.join(", ")}.`,
2814
2776
  });
2815
2777
  // Only inject into LLM context in BLOCKING mode
2816
- if (tscMode === "BLOCKING" && this.iterationSystemMsgCount < 5) {
2778
+ if (tscMode === "BLOCKING") {
2817
2779
  const truncated = tscOutput.length > 2000
2818
2780
  ? tscOutput.slice(0, 2000) + "\n[...tsc output truncated]"
2819
2781
  : tscOutput;
2820
- this.contextManager.addMessage({
2821
- role: "system",
2822
- content: `[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`,
2823
- });
2824
- this.iterationSystemMsgCount++;
2782
+ this.ephemeralHints.push(`[Auto-TSC] TypeScript errors detected after modifying ${tscFilesThisIteration.length} files:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix these type errors.`);
2825
2783
  }
2826
2784
  }
2827
2785
  else {
@@ -2879,12 +2837,8 @@ export class AgentLoop extends EventEmitter {
2879
2837
  (iterReflection.reflection.whatFailed.length > 0
2880
2838
  ? iterReflection.reflection.whatFailed.slice(0, 2).join("; ")
2881
2839
  : null);
2882
- if (insight && insight.length > 10 && this.iterationSystemMsgCount < 5) {
2883
- this.contextManager.addMessage({
2884
- role: "system",
2885
- content: `[Reflection] ${insight}`,
2886
- });
2887
- this.iterationSystemMsgCount++;
2840
+ if (insight && insight.length > 10) {
2841
+ this.ephemeralHints.push(`[Reflection] ${insight}`);
2888
2842
  }
2889
2843
  }
2890
2844
  catch {
@@ -2910,10 +2864,7 @@ export class AgentLoop extends EventEmitter {
2910
2864
  .filter(([, dim]) => dim.issues.length > 0)
2911
2865
  .flatMap(([, dim]) => dim.issues);
2912
2866
  if (issues.length > 0) {
2913
- this.contextManager.addMessage({
2914
- role: "system",
2915
- content: `[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`,
2916
- });
2867
+ this.ephemeralHints.push(`[Self-Reflection L1] Issues detected: ${issues.join(", ")}. Confidence: ${quickResult.confidence}`);
2917
2868
  this.emitSubagent("verifier", "done", `quick verification flagged ${issues.length} issues, confidence ${quickResult.confidence.toFixed(2)}`);
2918
2869
  }
2919
2870
  else {
@@ -2960,7 +2911,7 @@ export class AgentLoop extends EventEmitter {
2960
2911
  source: "agent",
2961
2912
  });
2962
2913
  }
2963
- else if (this.iterationSystemMsgCount < 5) {
2914
+ else {
2964
2915
  // retry, approach_change, scope_reduce → 복구 프롬프트 주입
2965
2916
  // Context Budget: consolidated error guidance (recovery + debug in one message)
2966
2917
  const recoveryPrompt = this.failureRecovery.buildRecoveryPrompt(decision, {
@@ -2978,7 +2929,6 @@ export class AgentLoop extends EventEmitter {
2978
2929
  const rootCauseAnalysis = this.selfDebugLoop.analyzeError(errorSummary);
2979
2930
  if (rootCauseAnalysis.confidence >= 0.5) {
2980
2931
  const debugStrategy = this.selfDebugLoop.selectStrategy(iteration - 2, []);
2981
- // Bug 3 fix: use dynamic test command detection instead of hardcoded "pnpm build"
2982
2932
  const testCmd = this.config.loop.testCommand
2983
2933
  ?? this.detectTestCommand();
2984
2934
  const debugPrompt = this.selfDebugLoop.buildFixPrompt(debugStrategy, {
@@ -2990,7 +2940,6 @@ export class AgentLoop extends EventEmitter {
2990
2940
  currentStrategy: debugStrategy,
2991
2941
  });
2992
2942
  debugSuffix = `\n\n[SelfDebug L${Math.min(iteration - 2, 5)}] Strategy: ${debugStrategy}\n${debugPrompt}`;
2993
- // Bug 2 fix: wire a real llmFixer so selfDebugLoop.debug() can call LLM
2994
2943
  if (debugStrategy !== "escalate") {
2995
2944
  this.llmFixerRunCount++;
2996
2945
  this.selfDebugLoop.debug({
@@ -3017,11 +2966,20 @@ export class AgentLoop extends EventEmitter {
3017
2966
  }
3018
2967
  }
3019
2968
  }
3020
- this.contextManager.addMessage({
3021
- role: "system",
3022
- content: recoveryPrompt + debugSuffix,
3023
- });
3024
- this.iterationSystemMsgCount++;
2969
+ this.ephemeralHints.push(recoveryPrompt + debugSuffix);
2970
+ }
2971
+ // CausalChainResolver: root cause analysis for code-related errors
2972
+ if (this.failureSigMemory && projectPath) {
2973
+ try {
2974
+ const resolver = new CausalChainResolver(this.failureSigMemory);
2975
+ const causal = await resolver.resolve(errorSummary, this.changedFiles, projectPath);
2976
+ if (causal && causal.confidence > 0.5) {
2977
+ this.ephemeralHints.push(`[ROOT_CAUSE] ${causal.suspectedRootCause} (confidence: ${causal.confidence.toFixed(2)})` +
2978
+ (causal.affectedFiles.length > 0 ? `\nAffected: ${causal.affectedFiles.join(", ")}` : "") +
2979
+ (causal.recommendedStrategy ? `\nStrategy: ${causal.recommendedStrategy}` : ""));
2980
+ }
2981
+ }
2982
+ catch { /* CausalChainResolver failure is non-fatal */ }
3025
2983
  }
3026
2984
  // HierarchicalPlanner 리플래닝도 시도
3027
2985
  if (this.activePlan) {
@@ -3032,7 +2990,7 @@ export class AgentLoop extends EventEmitter {
3032
2990
  ? this.tokenUsage.total / this.config.loop.totalTokenBudget
3033
2991
  : 0;
3034
2992
  // Context Budget: skip all optional injections at 85%+ budget
3035
- if (errorTriggerBudgetRatio <= 0.85 && this.iterationSystemMsgCount < 5) {
2993
+ if (errorTriggerBudgetRatio <= 0.85) {
3036
2994
  // SkillLearner: 학습된 스킬 중 현재 에러에 매칭되는 것 주입
3037
2995
  if (this.skillLearner && this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
3038
2996
  try {
@@ -3043,22 +3001,17 @@ export class AgentLoop extends EventEmitter {
3043
3001
  for (const skill of relevantSkills.slice(0, 1)) {
3044
3002
  if (this.activeSkillIds.includes(skill.id))
3045
3003
  continue;
3046
- this.contextManager.addMessage({
3047
- role: "system",
3048
- content: `[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`,
3049
- });
3004
+ this.ephemeralHints.push(`[Learned Skill: ${skill.id}] Diagnosis: ${skill.diagnosis}\nStrategy: ${skill.strategy}\nTools: ${skill.toolSequence.join(" → ")}`);
3050
3005
  this.activeSkillIds.push(skill.id);
3051
- this.iterationSystemMsgCount++;
3052
3006
  this.skillLearner.updateConfidence(skill.id, false);
3053
3007
  }
3054
3008
  }
3055
3009
  catch {
3056
- // 학습 스킬 매칭 실패는 치명적이지 않음
3010
+ /* non-fatal */
3057
3011
  }
3058
3012
  }
3059
3013
  // Plugin trigger matching — match errors/context to plugin skills
3060
- if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS &&
3061
- this.iterationSystemMsgCount < 5) {
3014
+ if (this.activeSkillIds.length < AgentLoop.MAX_ACTIVE_SKILLS) {
3062
3015
  const triggerMatches = this.pluginRegistry.matchTriggers({
3063
3016
  errorMessage: errorSummary,
3064
3017
  taskDescription: "",
@@ -3076,12 +3029,8 @@ export class AgentLoop extends EventEmitter {
3076
3029
  if (resolved.length > MAX_SKILL_INJECT_CHARS) {
3077
3030
  resolved = resolved.slice(0, MAX_SKILL_INJECT_CHARS) + "\n[...truncated for token budget]";
3078
3031
  }
3079
- this.contextManager.addMessage({
3080
- role: "system",
3081
- content: `[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`,
3082
- });
3032
+ this.ephemeralHints.push(`[Plugin Skill: ${bestMatch.pluginId}/${bestMatch.skill.name}]\n${resolved}`);
3083
3033
  this.activeSkillIds.push(bestMatch.skill.id);
3084
- this.iterationSystemMsgCount++;
3085
3034
  this.emitEvent({
3086
3035
  kind: "agent:thinking",
3087
3036
  content: `Activated plugin skill: ${bestMatch.skill.name} from ${bestMatch.pluginId}`,
@@ -3116,9 +3065,7 @@ export class AgentLoop extends EventEmitter {
3116
3065
  createdAt: new Date(),
3117
3066
  });
3118
3067
  }
3119
- catch {
3120
- // Checkpoint failure is non-fatal
3121
- }
3068
+ catch { /* non-fatal */ }
3122
3069
  }
3123
3070
  // ContinuousReflection: 매 5 iteration마다 비상 체크포인트 트리거
3124
3071
  // (정기 타이머 외에 iteration 기반 추가 안전망)
@@ -3128,9 +3075,7 @@ export class AgentLoop extends EventEmitter {
3128
3075
  try {
3129
3076
  await this.continuousReflection.emergencyCheckpoint();
3130
3077
  }
3131
- catch {
3132
- // reflection 체크포인트 실패는 치명적이지 않음
3133
- }
3078
+ catch { /* non-fatal */ }
3134
3079
  }
3135
3080
  // complex/massive task + 다중 변경 파일일 때만 aggregate impact hint 1회 주입
3136
3081
  if (!this.impactHintInjected &&
@@ -3139,6 +3084,7 @@ export class AgentLoop extends EventEmitter {
3139
3084
  (this.currentComplexity === "complex" || this.currentComplexity === "massive")) {
3140
3085
  await this.maybeInjectAggregateImpactHint();
3141
3086
  }
3087
+ this.flushEphemeralHints();
3142
3088
  // 예산 초과 체크
3143
3089
  if (this.tokenUsage.total >= this.config.loop.totalTokenBudget) {
3144
3090
  return {
@@ -3297,7 +3243,245 @@ export class AgentLoop extends EventEmitter {
3297
3243
  const args = this.parseToolArgs(toolCall.arguments);
3298
3244
  const allDefinitions = [...this.config.loop.tools, ...this.mcpToolDefinitions, SPAWN_SUB_AGENT_TOOL];
3299
3245
  const matchedDefinition = allDefinitions.find((t) => t.name === toolCall.name);
3300
- // Governor: 안전성 검증
3246
+ // Security gate — deterministic pattern check (shell injection, credential leaks, etc.)
3247
+ {
3248
+ const secResult = securityCheck(toolCall.name, args);
3249
+ if (secResult.verdict === "BLOCK") {
3250
+ this.ephemeralHints.push(`[SECURITY] Blocked: ${secResult.reason}`);
3251
+ // Log security block event to .yuan/logs/security-events.jsonl (non-fatal)
3252
+ try {
3253
+ const { mkdirSync, appendFileSync } = await import("node:fs");
3254
+ const logDir = pathJoin(this.config.loop.projectPath, ".yuan", "logs");
3255
+ mkdirSync(logDir, { recursive: true });
3256
+ const logPath = pathJoin(logDir, "security-events.jsonl");
3257
+ appendFileSync(logPath, JSON.stringify({
3258
+ tool: toolCall.name,
3259
+ verdict: "BLOCK",
3260
+ reason: secResult.reason,
3261
+ pattern: secResult.pattern,
3262
+ timestamp: Date.now(),
3263
+ }) + "\n");
3264
+ }
3265
+ catch { /* non-fatal */ }
3266
+ const blockResult = {
3267
+ tool_call_id: toolCall.id,
3268
+ name: toolCall.name,
3269
+ output: `[SECURITY_BLOCKED] ${secResult.reason}. This operation is not allowed.`,
3270
+ success: false,
3271
+ durationMs: 0,
3272
+ };
3273
+ return { result: blockResult, deferredFixPrompt: null };
3274
+ }
3275
+ if (secResult.verdict === "WARN") {
3276
+ this.ephemeralHints.push(`[SECURITY WARNING] ${secResult.reason} — proceeding with caution`);
3277
+ }
3278
+ }
3279
+ // Workspace Mutation Policy — path-level safety zones (after security gate)
3280
+ if (this.mutationPolicy && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
3281
+ const filePath = String(args.path ?? args.file_path ?? "");
3282
+ const mutation = this.mutationPolicy.check(filePath);
3283
+ if (!mutation.allowed) {
3284
+ return {
3285
+ result: {
3286
+ tool_call_id: toolCall.id,
3287
+ name: toolCall.name,
3288
+ output: `[MUTATION_BLOCKED] ${mutation.reason}`,
3289
+ success: false,
3290
+ durationMs: 0,
3291
+ },
3292
+ deferredFixPrompt: null,
3293
+ };
3294
+ }
3295
+ if (mutation.requiresApproval) {
3296
+ this.ephemeralHints.push(`[MUTATION] ${filePath} is in ${mutation.zone} zone — approval recommended`);
3297
+ }
3298
+ }
3299
+ // Judgment rules — deterministic rule-based tool approval (loaded from .yuan/judgment-rules.json)
3300
+ if (this.judgmentRegistry) {
3301
+ const judgment = this.judgmentRegistry.evaluate(toolCall.name, args);
3302
+ if (judgment.action === "BLOCK") {
3303
+ const judgmentResult = {
3304
+ tool_call_id: toolCall.id,
3305
+ name: toolCall.name,
3306
+ output: `[JUDGMENT_BLOCKED] ${judgment.reason}`,
3307
+ success: false,
3308
+ durationMs: 0,
3309
+ };
3310
+ return { result: judgmentResult, deferredFixPrompt: null };
3311
+ }
3312
+ if (judgment.action === "WARN") {
3313
+ this.ephemeralHints.push(`[JUDGMENT] ${judgment.reason}`);
3314
+ }
3315
+ if (judgment.action === "REQUIRE_APPROVAL") {
3316
+ this.ephemeralHints.push(`[JUDGMENT] ${judgment.reason} — approval recommended`);
3317
+ }
3318
+ }
3319
+ // ToolGate enforcement — block tools based on Decision Engine gate level
3320
+ {
3321
+ const gate = this.decision.core.toolGate;
3322
+ if (gate.blockedTools.includes(toolCall.name)) {
3323
+ const gateResult = {
3324
+ tool_call_id: toolCall.id,
3325
+ name: toolCall.name,
3326
+ output: `[TOOL_GATE] ${toolCall.name} is blocked in ${gate.level} mode.`,
3327
+ success: false,
3328
+ durationMs: 0,
3329
+ };
3330
+ return { result: gateResult, deferredFixPrompt: null };
3331
+ }
3332
+ }
3333
+ {
3334
+ const vetoFlags = this.decision.core.vetoFlags;
3335
+ // editVetoed → block file_edit/file_write tools
3336
+ if (vetoFlags.editVetoed && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
3337
+ const vetoResult = {
3338
+ tool_call_id: toolCall.id,
3339
+ name: toolCall.name,
3340
+ output: "[Decision Engine] Edit vetoed: patch risk is too high without a plan. Please create a plan first or ask the user for clarification.",
3341
+ success: false,
3342
+ durationMs: 0,
3343
+ };
3344
+ return { result: vetoResult, deferredFixPrompt: null };
3345
+ }
3346
+ }
3347
+ // Pre-edit quality gate: remind LLM to verify before writing
3348
+ {
3349
+ const cq = this.decision.core.codeQuality;
3350
+ if (cq.preEditVerify && (toolCall.name === "file_edit" || toolCall.name === "file_write")) {
3351
+ if (this.toolUsageCounter.edits === 0) {
3352
+ this.ephemeralHints.push(`[Code Quality] This is a ${cq.codeTaskType} task. ` +
3353
+ `Primary risk: ${cq.primaryRisk}. ` +
3354
+ `Verify your change is complete and correct before proceeding to the next file.`);
3355
+ }
3356
+ }
3357
+ }
3358
+ // Dependency guard — detect package installs and manifest changes
3359
+ {
3360
+ const depResult = checkDependencyChange(toolCall.name, args);
3361
+ if (depResult.isDependencyChange) {
3362
+ this.ephemeralHints.push(`[DEP CHANGE] ${depResult.reason}. Verify after: ${depResult.verifyAfter}`);
3363
+ if (depResult.requiresApproval) {
3364
+ this.ephemeralHints.push(`[DEP APPROVAL] Dependency change requires careful review. Budget multiplier: ${depResult.budgetMultiplier}x`);
3365
+ }
3366
+ if (depResult.kind) {
3367
+ const depFilePath = String(args.path ?? args.file_path ?? args.command ?? "");
3368
+ this.emitEvent({
3369
+ kind: "agent:dependency_change_detected",
3370
+ depKind: depResult.kind,
3371
+ path: depFilePath.slice(0, 200),
3372
+ requiresApproval: depResult.requiresApproval,
3373
+ });
3374
+ }
3375
+ }
3376
+ }
3377
+ {
3378
+ const budget = this.decision.core.toolBudget;
3379
+ const counter = this.toolUsageCounter;
3380
+ const toolName = toolCall.name;
3381
+ if (toolName === "file_read")
3382
+ counter.reads++;
3383
+ else if (toolName === "file_edit" || toolName === "file_write") {
3384
+ counter.edits++;
3385
+ const filePath = String(args.path ?? args.file ?? "");
3386
+ if (filePath) {
3387
+ const prev = counter.sameFileEdits.get(filePath) ?? 0;
3388
+ counter.sameFileEdits.set(filePath, prev + 1);
3389
+ if (prev + 1 > budget.maxSameFileEdits) {
3390
+ this.emitEvent({
3391
+ kind: "agent:budget_warning",
3392
+ tool: toolName,
3393
+ used: prev + 1,
3394
+ limit: budget.maxSameFileEdits,
3395
+ });
3396
+ this.contextManager.addMessage({
3397
+ role: "system",
3398
+ content: `[Budget Warning] Same-file edit limit approaching for ${filePath} (${prev + 1}/${budget.maxSameFileEdits}). Consider a different approach.`,
3399
+ });
3400
+ }
3401
+ }
3402
+ }
3403
+ else if (toolName === "shell_exec" || toolName === "bash") {
3404
+ counter.shells++;
3405
+ const cmd = String(args.command ?? "").toLowerCase();
3406
+ if (/\b(test|jest|vitest|mocha|pytest|cargo test|go test)\b/.test(cmd)) {
3407
+ counter.tests++;
3408
+ }
3409
+ // CommandPlanCompiler — validate LLM-proposed shell commands against deterministic compiled plan
3410
+ const rawCmd = String(args.command ?? "");
3411
+ const classified = classifyCommand(rawCmd);
3412
+ if (classified.purpose && this.config.loop.projectPath) {
3413
+ const compilerInput = {
3414
+ verifyDepth: this.decision.core.verifyDepth,
3415
+ packageManager: this.worldState?.deps?.packageManager ?? "npm",
3416
+ testFramework: "unknown",
3417
+ buildTool: "tsc",
3418
+ hasStrictMode: false,
3419
+ monorepo: false,
3420
+ };
3421
+ let compiled;
3422
+ if (classified.purpose === "verify")
3423
+ compiled = compileVerifyCommands(compilerInput);
3424
+ else if (classified.purpose === "build")
3425
+ compiled = compileVerifyCommands({ ...compilerInput, verifyDepth: "thorough" });
3426
+ else
3427
+ compiled = null;
3428
+ if (compiled && compiled.commands.length > 0) {
3429
+ const validation = validateProposedCommand(rawCmd, compiled);
3430
+ if (validation.recommendation === "use_compiled") {
3431
+ this.ephemeralHints.push(`[CMD_PLAN] Command mismatch: ${validation.deviation}. Compiled: ${compiled.commands[0]}`);
3432
+ }
3433
+ else if (validation.recommendation === "warn") {
3434
+ this.ephemeralHints.push(`[CMD_PLAN] Note: ${validation.deviation ?? "Minor deviation from compiled command"}`);
3435
+ }
3436
+ }
3437
+ }
3438
+ }
3439
+ else if (toolName === "grep" || toolName === "glob" || toolName === "file_search")
3440
+ counter.searches++;
3441
+ else if (toolName === "web_search" || toolName === "parallel_web_search")
3442
+ counter.webLookups++;
3443
+ else if (toolName === "test_run" || toolName === "run_tests")
3444
+ counter.tests++;
3445
+ const checks = [
3446
+ { name: "file_read", used: counter.reads, limit: budget.maxFileReads },
3447
+ { name: "file_edit", used: counter.edits, limit: budget.maxEdits },
3448
+ { name: "shell_exec", used: counter.shells, limit: budget.maxShellExecs },
3449
+ { name: "search", used: counter.searches, limit: budget.maxSearches },
3450
+ { name: "web_lookup", used: counter.webLookups, limit: budget.maxWebLookups },
3451
+ { name: "test_run", used: counter.tests, limit: budget.maxTestRuns },
3452
+ ];
3453
+ for (const check of checks) {
3454
+ if (check.limit <= 0)
3455
+ continue;
3456
+ const ratio = check.used / check.limit;
3457
+ if (ratio >= 1.0) {
3458
+ this.emitEvent({ kind: "agent:budget_exceeded", tool: check.name });
3459
+ const budgetResult = {
3460
+ tool_call_id: toolCall.id,
3461
+ name: toolCall.name,
3462
+ output: `[Decision Engine] Tool budget exceeded for ${check.name} (${check.used}/${check.limit}). Try a different approach or wrap up.`,
3463
+ success: false,
3464
+ durationMs: 0,
3465
+ };
3466
+ return { result: budgetResult, deferredFixPrompt: null };
3467
+ }
3468
+ if (ratio >= 0.8) {
3469
+ this.emitEvent({
3470
+ kind: "agent:budget_warning",
3471
+ tool: check.name,
3472
+ used: check.used,
3473
+ limit: check.limit,
3474
+ });
3475
+ if (this.iterationSystemMsgCount < 5) {
3476
+ this.contextManager.addMessage({
3477
+ role: "system",
3478
+ content: `[Budget Warning] ${check.name} usage at ${Math.round(ratio * 100)}% (${check.used}/${check.limit}). Consider wrapping up or using fewer ${check.name} calls.`,
3479
+ });
3480
+ this.iterationSystemMsgCount++;
3481
+ }
3482
+ }
3483
+ }
3484
+ }
3301
3485
  try {
3302
3486
  this.governor.validateToolCall(toolCall);
3303
3487
  }
@@ -3307,7 +3491,7 @@ export class AgentLoop extends EventEmitter {
3307
3491
  if (approvalResult) {
3308
3492
  return { result: approvalResult, deferredFixPrompt: null };
3309
3493
  }
3310
- // 승인됨 계속 실행
3494
+ // Approved continue execution
3311
3495
  }
3312
3496
  // Generic tool-definition approval gate
3313
3497
  if (matchedDefinition?.requiresApproval) {
@@ -3336,7 +3520,6 @@ export class AgentLoop extends EventEmitter {
3336
3520
  throw err;
3337
3521
  }
3338
3522
  }
3339
- // ApprovalManager: 추가 승인 체크
3340
3523
  const approvalRequest = this.approvalManager.checkApproval(toolCall.name, args);
3341
3524
  if (approvalRequest) {
3342
3525
  const approvalResult = await this.handleApprovalRequest(toolCall, approvalRequest);
@@ -3344,7 +3527,6 @@ export class AgentLoop extends EventEmitter {
3344
3527
  return { result: approvalResult, deferredFixPrompt: null };
3345
3528
  }
3346
3529
  }
3347
- // Plugin Tool Approval Gate
3348
3530
  const pluginTools = this.pluginRegistry.getAllTools();
3349
3531
  const matchedPluginTool = pluginTools.find((pt) => pt.tool.name === toolCall.name);
3350
3532
  if (matchedPluginTool &&
@@ -3365,12 +3547,10 @@ export class AgentLoop extends EventEmitter {
3365
3547
  return { result: pluginApprovalResult, deferredFixPrompt: null };
3366
3548
  }
3367
3549
  }
3368
- // spawn_sub_agent 도구 호출 — SubAgent로 위임
3369
3550
  if (toolCall.name === "spawn_sub_agent") {
3370
3551
  const subAgentResult = await this.executeSpawnSubAgent(toolCall, args);
3371
3552
  return { result: subAgentResult, deferredFixPrompt: null };
3372
3553
  }
3373
- // MCP 도구 호출 확인
3374
3554
  if (this.mcpClient && this.isMCPTool(toolCall.name)) {
3375
3555
  // Emit tool_start before execution (required for trace, QA pipeline, replay)
3376
3556
  this.emitEvent({
@@ -3380,7 +3560,6 @@ export class AgentLoop extends EventEmitter {
3380
3560
  source: "mcp",
3381
3561
  });
3382
3562
  const mcpResult = await this.executeMCPTool(toolCall);
3383
- // Normalize MCP result: wrap search tool output into structured JSON if applicable
3384
3563
  const normalizedOutput = this.normalizeMcpResult(toolCall.name, mcpResult.output);
3385
3564
  const finalResult = { ...mcpResult, output: normalizedOutput };
3386
3565
  this.emitEvent({
@@ -3394,7 +3573,43 @@ export class AgentLoop extends EventEmitter {
3394
3573
  this.emitEvent({ kind: "agent:reasoning_delta", text: `tool finished: ${toolCall.name}` });
3395
3574
  return { result: finalResult, deferredFixPrompt: null };
3396
3575
  }
3397
- // 도구 실행
3576
+ // Pre-write validator — quality gate before actual file write (after all gates)
3577
+ if ((toolCall.name === "file_edit" || toolCall.name === "file_write") && this.decision.core.codeQuality.strictMode) {
3578
+ const filePath = String(args.path ?? args.file_path ?? "");
3579
+ const content = String(args.content ?? args.new_string ?? "");
3580
+ if (content) {
3581
+ const fileRole = detectFileRole(filePath);
3582
+ const validation = validateBeforeWrite(content, { path: filePath, fileRole, changedHunksOnly: true });
3583
+ if (!validation.valid) {
3584
+ this.ephemeralHints.push(`[QUALITY_BLOCK] ${validation.blockedReason}: ${validation.issues.filter(i => i.severity === "error").map(i => i.message).join("; ")}`);
3585
+ // Record weakness patterns for learning
3586
+ if (this.weaknessTracker) {
3587
+ for (const issue of validation.issues.filter(i => i.severity === "error")) {
3588
+ const pattern = issue.message.includes("TODO") ? "todo_in_code"
3589
+ : issue.message.includes("empty function") ? "empty_function_body"
3590
+ : issue.message.includes("any") ? "any_type_usage"
3591
+ : issue.message.includes("console") ? "console_log_leak"
3592
+ : issue.message.includes("stub") ? "stub_implementation"
3593
+ : "unknown_quality_issue";
3594
+ this.weaknessTracker.record(pattern, `CRITICAL: ${issue.message}. Fix this BEFORE writing.`);
3595
+ }
3596
+ }
3597
+ return {
3598
+ result: {
3599
+ tool_call_id: toolCall.id,
3600
+ name: toolCall.name,
3601
+ output: `[QUALITY_BLOCKED] ${validation.blockedReason}. Fix these issues and try again.`,
3602
+ success: false,
3603
+ durationMs: 0,
3604
+ },
3605
+ deferredFixPrompt: null,
3606
+ };
3607
+ }
3608
+ if (validation.issues.length > 0) {
3609
+ this.ephemeralHints.push(`[QUALITY_WARN] ${validation.issues.map(i => i.message).join("; ")}`);
3610
+ }
3611
+ }
3612
+ }
3398
3613
  const startTime = Date.now();
3399
3614
  const toolAbort = new AbortController();
3400
3615
  this.interruptManager.registerToolAbort(toolAbort);
@@ -3403,6 +3618,10 @@ export class AgentLoop extends EventEmitter {
3403
3618
  args.file;
3404
3619
  if (candidatePath) {
3405
3620
  const filePathStr = String(candidatePath);
3621
+ // Record before snapshot in patch journal for atomic rollback
3622
+ if (this.patchJournal) {
3623
+ this.patchJournal.recordBefore(toolCall.name, filePathStr);
3624
+ }
3406
3625
  if (!this.originalSnapshots.has(filePathStr)) {
3407
3626
  try {
3408
3627
  const { readFile } = await import("node:fs/promises");
@@ -3416,7 +3635,6 @@ export class AgentLoop extends EventEmitter {
3416
3635
  }
3417
3636
  }
3418
3637
  }
3419
- // Emit search_start for web search tools so TUI can show search progress
3420
3638
  const isSearchTool = toolCall.name === "web_search" || toolCall.name === "parallel_web_search";
3421
3639
  if (isSearchTool) {
3422
3640
  const searchArgs = typeof toolCall.arguments === "string"
@@ -3437,7 +3655,6 @@ export class AgentLoop extends EventEmitter {
3437
3655
  try {
3438
3656
  const result = await this.toolExecutor.execute(toolCall, toolAbort?.signal);
3439
3657
  this.interruptManager.clearToolAbort();
3440
- // Emit search_result after completion
3441
3658
  if (isSearchTool && result.success) {
3442
3659
  const searchArgs = typeof toolCall.arguments === "string"
3443
3660
  ? (() => { try {
@@ -3484,7 +3701,6 @@ export class AgentLoop extends EventEmitter {
3484
3701
  });
3485
3702
  this.reasoningTree.add("tool", `success: ${toolCall.name}`);
3486
3703
  if (["file_write", "file_edit"].includes(toolCall.name) && result.success) {
3487
- // Phase transition: explore → implement on first write
3488
3704
  if (this.taskPhase === "explore") {
3489
3705
  this.transitionPhase("implement", `first write: ${toolCall.name}`);
3490
3706
  }
@@ -3494,45 +3710,79 @@ export class AgentLoop extends EventEmitter {
3494
3710
  const filePathStr = String(filePath);
3495
3711
  if (!this.changedFiles.includes(filePathStr)) {
3496
3712
  this.changedFiles.push(filePathStr);
3497
- // Cap changedFiles to prevent unbounded growth on massive refactors
3498
3713
  if (this.changedFiles.length > BOUNDS.changedFiles) {
3499
3714
  this.changedFiles = this.changedFiles.slice(-BOUNDS.changedFiles);
3500
3715
  }
3501
3716
  }
3502
- // Task 2: track write tool paths per-iteration for QA triggering
3503
3717
  if (!this.iterationWriteToolPaths.includes(filePathStr)) {
3504
3718
  this.iterationWriteToolPaths.push(filePathStr);
3505
3719
  }
3506
- // Task 3: track TS/TSX files modified this iteration for auto-tsc
3507
3720
  if (filePathStr.match(/\.[cm]?tsx?$/) && !this.iterationTsFilesModified.includes(filePathStr)) {
3508
3721
  this.iterationTsFilesModified.push(filePathStr);
3509
3722
  }
3510
3723
  this.emitEvent({ kind: "agent:file_change", path: filePathStr, diff: result.output });
3511
- // Emit evidence report (async, fire-and-forget — no loop complexity added)
3512
3724
  this.emitEvidenceReport(filePathStr, toolCall.name).catch(() => { });
3513
- // Update world state after file modification
3725
+ // SemanticDiffReviewer classify change meaning and recommend verification depth
3726
+ if (this.originalSnapshots.has(filePathStr)) {
3727
+ try {
3728
+ const { readFile } = await import("node:fs/promises");
3729
+ const newContent = await readFile(filePathStr, "utf-8");
3730
+ const oldContent = this.originalSnapshots.get(filePathStr) ?? "";
3731
+ const review = reviewFileDiff({
3732
+ path: filePathStr,
3733
+ oldContent,
3734
+ newContent,
3735
+ language: filePathStr.match(/\.[cm]?tsx?$/) ? "typescript" : undefined,
3736
+ });
3737
+ if (review.recommendedRiskBoost > 0) {
3738
+ this.ephemeralHints.push(`[SEMANTIC_DIFF] ${filePathStr}: ${review.changes.join(", ")} — risk +${review.recommendedRiskBoost.toFixed(2)}, recommend verify=${review.recommendedVerifyDepth}`);
3739
+ }
3740
+ }
3741
+ catch { /* non-fatal — file may have been deleted */ }
3742
+ }
3514
3743
  if (this.config.loop.projectPath) {
3515
3744
  const wsProjectPath = this.config.loop.projectPath;
3516
3745
  new WorldStateCollector({ projectPath: wsProjectPath, skipTest: true })
3517
3746
  .collect()
3518
3747
  .then((snapshot) => { this.worldState = snapshot; })
3519
- .catch(() => {
3520
- // Non-fatal: world state update failure should not interrupt tool execution
3521
- });
3748
+ .catch(() => { });
3522
3749
  }
3523
3750
  if (this.impactAnalyzer) {
3524
- this.analyzeFileImpact(filePathStr).catch((err) => {
3525
- // Non-fatal: impact analysis failure should not interrupt tool execution
3526
- const msg = err instanceof Error ? err.message : String(err);
3527
- // file-only log console.warn corrupts Ink TUI
3751
+ this.analyzeFileImpact(filePathStr).catch(() => { });
3752
+ }
3753
+ // Auto-create rollback point based on Decision failureSurface
3754
+ if (this.patchJournal && this.patchJournal.shouldCreateRollbackPoint(this.decision.core.failureSurface.patchRisk, this.changedFiles.length)) {
3755
+ const point = this.patchJournal.createRollbackPoint(`patchRisk=${this.decision.core.failureSurface.patchRisk}, files=${this.changedFiles.length}`);
3756
+ this.emitEvent({
3757
+ kind: "agent:rollback_point_created",
3758
+ pointId: point.id,
3759
+ reason: point.reason,
3528
3760
  });
3529
3761
  }
3762
+ // Patch scope tracking (after successful file mutation)
3763
+ if (this.patchScopeController) {
3764
+ const content = String(args.content ?? args.new_string ?? "");
3765
+ const isProtected = this.mutationPolicy?.check(filePathStr).zone === "PROTECTED" || this.mutationPolicy?.check(filePathStr).zone === "CAUTION";
3766
+ this.patchScopeController.recordChange(filePathStr, content.split("\n").length, 0, isProtected ?? false);
3767
+ const scopeCheck = this.patchScopeController.check();
3768
+ if (!scopeCheck.allowed) {
3769
+ this.ephemeralHints.push(`[SCOPE_LIMIT] ${scopeCheck.reason}. Consider splitting into smaller tasks or requesting plan escalation.`);
3770
+ }
3771
+ }
3772
+ }
3773
+ // Verifier rules — deterministic check of tool output for success/failure patterns
3774
+ {
3775
+ const verification = verifyToolResult(toolCall.name, args, result.output, result.success);
3776
+ if (verification.verdict === "FAIL") {
3777
+ this.ephemeralHints.push(`[VERIFY FAIL] ${verification.reason}. Suggested: ${verification.suggestedAction}`);
3778
+ }
3779
+ if (verification.verdict === "WARN") {
3780
+ this.ephemeralHints.push(`[VERIFY WARN] ${verification.reason}`);
3781
+ }
3530
3782
  }
3531
- // StateUpdater: sync world model with actual tool execution result
3532
3783
  if (this.stateUpdater && result.success) {
3533
3784
  this.stateUpdater.applyToolResult(toolCall.name, args, result).catch(() => { });
3534
3785
  }
3535
- // Extract edited file path so auto-fix can lint only the changed file
3536
3786
  const editedFilePath = args?.path
3537
3787
  ?? args?.file_path
3538
3788
  ?? undefined;
@@ -3853,10 +4103,6 @@ export class AgentLoop extends EventEmitter {
3853
4103
  };
3854
4104
  }
3855
4105
  }
3856
- /**
3857
- * Governor의 ApprovalRequiredError를 ApprovalManager로 처리.
3858
- * 승인되면 null 반환 (실행 계속), 거부되면 ToolResult 반환 (실행 차단).
3859
- */
3860
4106
  async handleApproval(toolCall, args, err) {
3861
4107
  const request = {
3862
4108
  id: randomUUID(),
@@ -3896,20 +4142,15 @@ export class AgentLoop extends EventEmitter {
3896
4142
  * 실패 시 수정 프롬프트를 대화 히스토리에 추가.
3897
4143
  */
3898
4144
  async validateAndFeedback(toolName, result, filePath) {
3899
- // file_write/file_edit/shell_exec만 검증
3900
4145
  if (!["file_write", "file_edit", "shell_exec"].includes(toolName)) {
3901
4146
  return null;
3902
4147
  }
3903
4148
  const validation = await this.autoFixLoop.validateResult(toolName, result.output, result.success, this.config.loop.projectPath, filePath);
3904
4149
  if (validation.passed) {
3905
- // 검증 통과 → 수정 시도 기록 초기화
3906
4150
  this.autoFixLoop.resetAttempts();
3907
4151
  return null;
3908
4152
  }
3909
- // 검증 실패 → 에러 피드백
3910
4153
  if (!this.autoFixLoop.canRetry()) {
3911
- // 재시도 한도 초과 → 에러 event emit + reset + 에이전트에게 피드백 반환
3912
- // (return null 대신 피드백 프롬프트를 반환하여 에이전트가 다른 접근법 시도 가능)
3913
4154
  const failMsg = validation.failures
3914
4155
  .map((f) => `[${f.type}] ${f.message}`)
3915
4156
  .join("; ");
@@ -3920,14 +4161,11 @@ export class AgentLoop extends EventEmitter {
3920
4161
  this.autoFixLoop.resetAttempts();
3921
4162
  return `[LINT VALIDATION FAILED after 3 auto-fix attempts — SKIP lint for this file and continue with the main task. Do NOT retry fixing lint errors. Move on to the next step.]\nFailures: ${failMsg}`;
3922
4163
  }
3923
- // 수정 프롬프트 생성 — 히스토리 추가는 caller가 tool result 추가 후 수행
3924
4164
  const errorMsg = validation.failures
3925
4165
  .map((f) => `[${f.type}] ${f.message}\n${f.rawOutput}`)
3926
4166
  .join("\n\n");
3927
4167
  const fixPrompt = this.autoFixLoop.buildFixPrompt(errorMsg, `After ${toolName} execution on project at ${this.config.loop.projectPath}`);
3928
- // 수정 시도 기록
3929
4168
  this.autoFixLoop.recordAttempt(errorMsg, "Requesting LLM fix", false, 0);
3930
- // fixPrompt를 반환 — caller가 tool result 메시지 추가 후 context에 넣음
3931
4169
  return fixPrompt;
3932
4170
  }
3933
4171
  /**
@@ -3948,11 +4186,6 @@ export class AgentLoop extends EventEmitter {
3948
4186
  }
3949
4187
  return args;
3950
4188
  }
3951
- // ─── Continuation Helpers ───
3952
- /**
3953
- * 토큰 예산 소진 임박 시 자동 체크포인트를 저장한다.
3954
- * 현재 진행 상태, 변경 파일, 에러 등을 직렬화.
3955
- */
3956
4189
  async saveAutoCheckpoint(iteration) {
3957
4190
  if (!this.continuationEngine)
3958
4191
  return;
@@ -3985,14 +4218,8 @@ export class AgentLoop extends EventEmitter {
3985
4218
  });
3986
4219
  }
3987
4220
  }
3988
- catch {
3989
- // 체크포인트 저장 실패는 치명적이지 않음
3990
- }
4221
+ catch { /* non-fatal */ }
3991
4222
  }
3992
- /**
3993
- * 매 iteration 시작 시 현재 플랜 진행 상황을 컨텍스트에 주입.
3994
- * 같은 태스크 인덱스라면 3 iteration마다만 주입 (컨텍스트 bloat 방지).
3995
- */
3996
4223
  injectPlanProgress(iteration) {
3997
4224
  if (!this.activePlan)
3998
4225
  return;
@@ -4030,16 +4257,8 @@ export class AgentLoop extends EventEmitter {
4030
4257
  else {
4031
4258
  lines.push(`\nAll tasks complete — verify and wrap up.`);
4032
4259
  }
4033
- this.contextManager.addMessage({
4034
- role: "system",
4035
- content: lines.join("\n"),
4036
- });
4037
- this.iterationSystemMsgCount++;
4260
+ this.ephemeralHints.push(lines.join("\n"));
4038
4261
  }
4039
- /**
4040
- * 현재 태스크의 targetFiles가 changedFiles에 포함됐는지 확인해
4041
- * 완료 감지 시 다음 태스크로 자동 전진.
4042
- */
4043
4262
  tryAdvancePlanTask() {
4044
4263
  if (!this.activePlan)
4045
4264
  return;
@@ -4148,12 +4367,6 @@ export class AgentLoop extends EventEmitter {
4148
4367
  getWorldState() {
4149
4368
  return this.worldState;
4150
4369
  }
4151
- // ─── Interrupt Helpers ───
4152
- /**
4153
- * InterruptManager 이벤트를 AgentLoop에 연결한다.
4154
- * - soft interrupt: 피드백을 user 메시지로 주입
4155
- * - hard interrupt: 루프 즉시 중단
4156
- */
4157
4370
  setupInterruptListeners() {
4158
4371
  // soft interrupt: 피드백을 대화 히스토리에 주입
4159
4372
  this.interruptManager.on("interrupt:feedback", (feedback) => {
@@ -4167,10 +4380,6 @@ export class AgentLoop extends EventEmitter {
4167
4380
  this.aborted = true;
4168
4381
  });
4169
4382
  }
4170
- /**
4171
- * pause 상태일 때 resume 시그널을 대기한다.
4172
- * resume 또는 hard interrupt가 올 때까지 블로킹.
4173
- */
4174
4383
  waitForResume() {
4175
4384
  return new Promise((resolve) => {
4176
4385
  // 이미 resume 상태이면 즉시 반환
@@ -4192,11 +4401,6 @@ export class AgentLoop extends EventEmitter {
4192
4401
  this.interruptManager.on("interrupt:hard", onHard);
4193
4402
  });
4194
4403
  }
4195
- // ─── Impact Analysis ───
4196
- /**
4197
- * Returns a cached ImpactReport if the same files were already analyzed,
4198
- * otherwise computes and caches a fresh report.
4199
- */
4200
4404
  async getOrComputeImpact(files) {
4201
4405
  if (!this.impactAnalyzer || files.length === 0)
4202
4406
  return null;
@@ -4229,12 +4433,8 @@ export class AgentLoop extends EventEmitter {
4229
4433
  kind: "agent:thinking",
4230
4434
  content: `Impact analysis: ${report.riskLevel} risk. ${report.affectedFiles.length} affected files, ${report.breakingChanges.length} breaking changes.`,
4231
4435
  });
4232
- // 고위험 변경 정보를 LLM에 주입
4233
4436
  const impactPrompt = this.impactAnalyzer.formatForPrompt(report);
4234
- this.contextManager.addMessage({
4235
- role: "system",
4236
- content: impactPrompt,
4237
- });
4437
+ this.ephemeralHints.push(impactPrompt);
4238
4438
  }
4239
4439
  }
4240
4440
  /**
@@ -4269,10 +4469,7 @@ export class AgentLoop extends EventEmitter {
4269
4469
  if (planPreview) {
4270
4470
  hintLines.push("", "Suggested refactor order:", planPreview);
4271
4471
  }
4272
- this.contextManager.addMessage({
4273
- role: "system",
4274
- content: hintLines.join("\n"),
4275
- });
4472
+ this.ephemeralHints.push(hintLines.join("\n"));
4276
4473
  this.impactHintInjected = true;
4277
4474
  this.emitEvent({
4278
4475
  kind: "agent:thinking",
@@ -4281,14 +4478,8 @@ export class AgentLoop extends EventEmitter {
4281
4478
  `${report.deadCodeCandidates.length} dead code candidates.`,
4282
4479
  });
4283
4480
  }
4284
- catch {
4285
- // aggregate impact 실패는 치명적이지 않음
4286
- }
4481
+ catch { /* non-fatal */ }
4287
4482
  }
4288
- /**
4289
- * 종료 직전 최종 impact 요약 생성.
4290
- * assistant final summary에만 붙이고 system prompt 오염은 하지 않는다.
4291
- */
4292
4483
  async buildFinalImpactSummary() {
4293
4484
  if (!this.impactAnalyzer || this.changedFiles.length === 0)
4294
4485
  return null;
@@ -4340,14 +4531,6 @@ export class AgentLoop extends EventEmitter {
4340
4531
  const args = this.parseToolArgs(toolCall.arguments);
4341
4532
  return this.mcpClient.callToolAsYuan(toolCall.name, args, toolCall.id);
4342
4533
  }
4343
- /**
4344
- * Normalize an MCP tool result for consistent downstream consumption.
4345
- *
4346
- * - Search tools (tool name contains "search"): if the output is unstructured text,
4347
- * wrap it into a JSON array of `{ title, url, snippet, source }` objects.
4348
- * Each non-empty line is treated as a snippet entry when structured data is absent.
4349
- * - All other tools: pass through unchanged.
4350
- */
4351
4534
  normalizeMcpResult(toolName, output) {
4352
4535
  const isSearch = /search/i.test(toolName);
4353
4536
  if (!isSearch)
@@ -4378,11 +4561,6 @@ export class AgentLoop extends EventEmitter {
4378
4561
  });
4379
4562
  return JSON.stringify(structured, null, 2);
4380
4563
  }
4381
- /**
4382
- * ContinuousReflection overflow signal을 soft rollover로 처리한다.
4383
- * 절대 abort하지 않고, 체크포인트 저장 후 다음 iteration에서
4384
- * ContextManager가 압축된 컨텍스트를 사용하도록 둔다.
4385
- */
4386
4564
  async handleSoftContextOverflow() {
4387
4565
  try {
4388
4566
  await this.saveAutoCheckpoint(this.iterationCount);
@@ -4409,7 +4587,6 @@ export class AgentLoop extends EventEmitter {
4409
4587
  }
4410
4588
  this.traceRecorder?.stop();
4411
4589
  }
4412
- // ─── Helpers ───
4413
4590
  /**
4414
4591
  * Builds a Map<filePath, toolOutput> for all changed files from write/edit tool results.
4415
4592
  * Used by selfReflection deepVerify and quickVerify.
@@ -4463,11 +4640,6 @@ export class AgentLoop extends EventEmitter {
4463
4640
  });
4464
4641
  return { reason: "ERROR", error: message };
4465
4642
  }
4466
- // ─── Task phase transitions ───────────────────────────────────────────────
4467
- /**
4468
- * Deterministic phase transition — no LLM involved.
4469
- * Emits agent:phase_transition event for TUI trace.
4470
- */
4471
4643
  transitionPhase(to, trigger) {
4472
4644
  if (this.taskPhase === to)
4473
4645
  return;
@@ -4481,10 +4653,6 @@ export class AgentLoop extends EventEmitter {
4481
4653
  trigger,
4482
4654
  });
4483
4655
  }
4484
- /**
4485
- * Cheap local checks run in verify phase — no LLM.
4486
- * Returns true if all checks pass (safe to advance to finalize).
4487
- */
4488
4656
  async runCheapChecks() {
4489
4657
  const projectPath = this.config.loop.projectPath;
4490
4658
  // 1. Diff size check — if nothing changed, skip
@@ -4517,16 +4685,10 @@ export class AgentLoop extends EventEmitter {
4517
4685
  if (hasErrors) {
4518
4686
  // cheap check: tsc errors found — staying in verify
4519
4687
  // Inject TS errors so LLM sees them and fixes them
4520
- if (this.iterationSystemMsgCount < 5) {
4521
- const truncated = result.output.length > 1000
4522
- ? result.output.slice(0, 1000) + "\n[truncated]"
4523
- : result.output;
4524
- this.contextManager.addMessage({
4525
- role: "system",
4526
- content: `[Verify Phase] TypeScript errors found:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix before completion.`,
4527
- });
4528
- this.iterationSystemMsgCount++;
4529
- }
4688
+ const truncated = result.output.length > 1000
4689
+ ? result.output.slice(0, 1000) + "\n[truncated]"
4690
+ : result.output;
4691
+ this.ephemeralHints.push(`[Verify Phase] TypeScript errors found:\n\`\`\`\n${truncated}\n\`\`\`\nPlease fix before completion.`);
4530
4692
  return false;
4531
4693
  }
4532
4694
  }
@@ -4537,11 +4699,6 @@ export class AgentLoop extends EventEmitter {
4537
4699
  }
4538
4700
  return true;
4539
4701
  }
4540
- // ─── Evidence Report ───────────────────────────────────────────────────────
4541
- /**
4542
- * After a file write/edit, collect diff stats + syntax signal and emit
4543
- * `agent:evidence_report`. Runs async and never blocks the main loop.
4544
- */
4545
4702
  async emitEvidenceReport(filePath, tool) {
4546
4703
  const timestamp = Date.now();
4547
4704
  const projectPath = this.config.loop.projectPath;
@@ -4565,10 +4722,6 @@ export class AgentLoop extends EventEmitter {
4565
4722
  }
4566
4723
  }
4567
4724
  catch { /* non-fatal */ }
4568
- // 2. Syntax check — skipped here to avoid spawning a duplicate tsc process.
4569
- // runCheapChecks() already runs tsc at the implement→verify boundary and
4570
- // the result flows to the QA pipeline. Spawning tsc per-write would
4571
- // create N parallel tsc processes for bulk refactors.
4572
4725
  const syntax = "skipped";
4573
4726
  this.emitEvent({
4574
4727
  kind: "agent:evidence_report",
@@ -4579,13 +4732,11 @@ export class AgentLoop extends EventEmitter {
4579
4732
  lintResult: "skipped",
4580
4733
  timestamp,
4581
4734
  });
4582
- // If a dep-graph file changed, invalidate arch summary cache
4583
4735
  const isDepsChange = filePath.includes("package.json") || filePath.includes("tsconfig");
4584
4736
  if (isDepsChange && this.archSummarizer) {
4585
4737
  this.archSummarizer.regenerate().catch(() => { });
4586
4738
  }
4587
4739
  }
4588
- // ─── OverheadGovernor helpers ─────────────────────────────────────────────
4589
4740
  /**
4590
4741
  * 현재 런타임 상태로 TriggerContext 빌드.
4591
4742
  */
@@ -4624,15 +4775,8 @@ export class AgentLoop extends EventEmitter {
4624
4775
  this.repeatedErrorSignature = undefined;
4625
4776
  }
4626
4777
  }
4627
- // ─── Message pruning — GPT recommendation ────────────────────────────────
4628
4778
  static MAX_MESSAGES = 40;
4629
4779
  static PRUNE_KEEP_RECENT = 10;
4630
- /**
4631
- * 메시지 배열이 MAX_MESSAGES를 초과하면 prune.
4632
- * 구조: [system] + [task summary] + [last PRUNE_KEEP_RECENT turns]
4633
- *
4634
- * "turns" = user/assistant 쌍 (tool messages는 해당 assistant 메시지에 귀속)
4635
- */
4636
4780
  pruneMessagesIfNeeded() {
4637
4781
  const msgs = this.contextManager.getMessages();
4638
4782
  if (msgs.length <= AgentLoop.MAX_MESSAGES)