anvil-dev-framework 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +71 -22
  2. package/VERSION +1 -1
  3. package/docs/ANV-263-hook-logging-investigation.md +116 -0
  4. package/docs/command-reference.md +398 -17
  5. package/docs/session-workflow.md +62 -9
  6. package/docs/system-architecture.md +584 -0
  7. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  8. package/global/api/openapi.yaml +357 -0
  9. package/global/api/ralph_api.py +528 -0
  10. package/global/commands/anvil-settings.md +47 -19
  11. package/global/commands/audit.md +163 -0
  12. package/global/commands/checklist.md +180 -0
  13. package/global/commands/coderabbit-fix.md +282 -0
  14. package/global/commands/efficiency.md +356 -0
  15. package/global/commands/evidence.md +117 -33
  16. package/global/commands/hud.md +24 -0
  17. package/global/commands/insights.md +101 -3
  18. package/global/commands/orient.md +22 -21
  19. package/global/commands/patterns.md +115 -0
  20. package/global/commands/ralph.md +47 -1
  21. package/global/commands/token-budget.md +214 -0
  22. package/global/commands/weekly-review.md +21 -1
  23. package/global/config/notifications.yaml.template +50 -0
  24. package/global/hooks/ralph_stop.sh +33 -1
  25. package/global/hooks/statusline.sh +67 -2
  26. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  29. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  30. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  31. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  32. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  33. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  34. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  35. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  36. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  37. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  38. package/global/lib/coderabbit_metrics.py +647 -0
  39. package/global/lib/command_tracker.py +147 -0
  40. package/global/lib/context_optimizer.py +323 -0
  41. package/global/lib/linear_provider.py +210 -16
  42. package/global/lib/log_rotation.py +287 -0
  43. package/global/lib/optimization_applier.py +582 -0
  44. package/global/lib/ralph_events.py +398 -0
  45. package/global/lib/ralph_notifier.py +366 -0
  46. package/global/lib/ralph_state.py +264 -24
  47. package/global/lib/ralph_webhooks.py +470 -0
  48. package/global/lib/state_manager.py +121 -0
  49. package/global/lib/token_analyzer.py +1383 -0
  50. package/global/lib/token_metrics.py +919 -0
  51. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
  54. package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  55. package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  63. package/global/tests/test_command_tracker.py +172 -0
  64. package/global/tests/test_context_optimizer.py +321 -0
  65. package/global/tests/test_linear_filtering.py +319 -0
  66. package/global/tests/test_linear_provider.py +40 -1
  67. package/global/tests/test_optimization_applier.py +508 -0
  68. package/global/tests/test_token_analyzer.py +735 -0
  69. package/global/tests/test_token_analyzer_phase6.py +537 -0
  70. package/global/tests/test_token_metrics.py +829 -0
  71. package/global/tools/README.md +153 -0
  72. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  73. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  74. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  75. package/global/tools/anvil-hud.py +86 -1
  76. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  77. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  78. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  79. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  80. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  81. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  82. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  83. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  84. package/global/tools/anvil-memory/src/db.ts +108 -0
  85. package/global/tools/anvil-memory/src/index.ts +2 -8
  86. package/global/tools/orient_linear.py +159 -0
  87. package/global/tools/ralph-watch +423 -0
  88. package/package.json +2 -1
  89. package/project/.anvil-project.yaml.template +93 -0
  90. package/project/CLAUDE.md.template +343 -0
  91. package/project/agents/README.md +119 -0
  92. package/project/agents/cross-layer-debugger.md +217 -0
  93. package/project/agents/security-code-reviewer.md +162 -0
  94. package/project/constitution.md.template +235 -0
  95. package/project/coordination.md +103 -0
  96. package/project/docs/background-tasks.md +258 -0
  97. package/project/docs/skills-frontmatter.md +243 -0
  98. package/project/examples/README.md +106 -0
  99. package/project/examples/api-route-template.ts +171 -0
  100. package/project/examples/component-template.tsx +110 -0
  101. package/project/examples/hook-template.ts +152 -0
  102. package/project/examples/service-template.ts +207 -0
  103. package/project/examples/test-template.test.tsx +249 -0
  104. package/project/hooks/README.md +491 -0
  105. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  106. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  107. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  108. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  109. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  110. package/project/hooks/notification.py +183 -0
  111. package/project/hooks/permission_request.py +438 -0
  112. package/project/hooks/post_tool_use.py +397 -0
  113. package/project/hooks/pre_compact.py +126 -0
  114. package/project/hooks/pre_tool_use.py +454 -0
  115. package/project/hooks/session_start.py +656 -0
  116. package/project/hooks/stop.py +356 -0
  117. package/project/hooks/subagent_start.py +223 -0
  118. package/project/hooks/subagent_stop.py +215 -0
  119. package/project/hooks/user_prompt_submit.py +110 -0
  120. package/project/hooks/utils/llm/anth.py +114 -0
  121. package/project/hooks/utils/llm/oai.py +114 -0
  122. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  123. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  124. package/project/hooks/utils/tts/openai_tts.py +92 -0
  125. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  126. package/project/linear.yaml.template +23 -0
  127. package/project/product.md.template +238 -0
  128. package/project/retros/README.md +126 -0
  129. package/project/rules/README.md +90 -0
  130. package/project/rules/debugging.md +139 -0
  131. package/project/rules/security-review.md +115 -0
  132. package/project/settings.yaml.template +185 -0
  133. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  134. package/project/templates/api-python/CLAUDE.md +547 -0
  135. package/project/templates/generic/CLAUDE.md +260 -0
  136. package/project/templates/saas/CLAUDE.md +478 -0
  137. package/project/tests/README.md +140 -0
  138. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  139. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  140. package/project/tests/test-hooks.sh +259 -0
  141. package/project/tests/test-lib.sh +248 -0
  142. package/project/tests/test-statusline.sh +165 -0
  143. package/project/tests/test_transcript_parser.py +323 -0
@@ -0,0 +1,1383 @@
1
+ """
2
+ Token Analyzer Service for Anvil Framework.
3
+
4
+ Provides analysis and recommendation generation for token consumption data.
5
+ Used by /audit and /efficiency commands.
6
+
7
+ Usage:
8
+ from token_analyzer import TokenAnalyzer
9
+
10
+ analyzer = TokenAnalyzer()
11
+ analysis = analyzer.analyze_session()
12
+ recommendations = analyzer.generate_recommendations(analysis)
13
+ """
14
+
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from typing import Optional, Dict, Any, List
18
+ from datetime import datetime, timezone
19
+
20
+ from token_metrics import TokenMetrics, SessionSummary
21
+
22
+ # Module logger
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # Claude context limits (approximate)
27
+ CONTEXT_LIMIT_TOKENS = 200000
28
+ EFFECTIVE_LIMIT_TOKENS = 150000 # Leave room for responses
29
+
30
+ # Component types that provide value through execution, not reference.
31
+ # These should not be flagged as "unused" even if was_used=FALSE.
32
+ EXECUTION_VALUE_TYPES = {"hook", "context"}
33
+
34
+
35
+ @dataclass
36
+ class WastePattern:
37
+ """A detected inefficiency pattern."""
38
+ pattern_type: str # redundant_load, unused_component, high_cost_low_value
39
+ component_name: str
40
+ component_type: str
41
+ tokens_wasted: int
42
+ occurrences: int
43
+ description: str
44
+
45
+
46
+ @dataclass
47
+ class Recommendation:
48
+ """An actionable optimization suggestion."""
49
+ priority: int # 1=high, 2=medium, 3=low
50
+ category: str # reduce, defer, remove, optimize
51
+ title: str
52
+ description: str
53
+ estimated_savings: int # tokens
54
+ action: Optional[str] = None # specific action to take
55
+
56
+
57
+ @dataclass
58
+ class SessionAnalysis:
59
+ """Complete analysis of a session's token consumption."""
60
+ session_id: str
61
+ summary: SessionSummary
62
+ context_percent_used: float
63
+ breakdown_by_type: Dict[str, Dict[str, Any]]
64
+ top_consumers: List[Dict[str, Any]]
65
+ waste_patterns: List[WastePattern]
66
+ efficiency_score: float # 0-100
67
+ analyzed_at: datetime = field(default_factory=datetime.now)
68
+
69
+
70
+ @dataclass
71
+ class ComponentEfficiency:
72
+ """Efficiency metrics for a single component."""
73
+ component_name: str
74
+ component_type: str
75
+ total_tokens: int
76
+ load_count: int
77
+ used_count: int
78
+ utilization_rate: float
79
+ efficiency_score: float
80
+ avg_tokens_per_load: float
81
+ trend: str # improving, stable, degrading, new
82
+
83
+
84
+ @dataclass
85
+ class EfficiencyReport:
86
+ """Historical efficiency analysis report."""
87
+ period_days: int
88
+ generated_at: datetime
89
+ total_sessions: int
90
+ total_tokens: int
91
+ avg_tokens_per_session: float
92
+ overall_efficiency_score: float
93
+ component_scores: List["ComponentEfficiency"]
94
+ trends: Dict[str, str]
95
+ recommendations: List[Recommendation]
96
+ comparison_to_previous: Optional[Dict[str, float]] = None
97
+
98
+
99
+ class TokenAnalyzer:
100
+ """
101
+ Token consumption analysis service.
102
+
103
+ Analyzes session data to detect waste patterns and generate
104
+ actionable optimization recommendations.
105
+ """
106
+
107
+ def __init__(self, metrics: Optional[TokenMetrics] = None):
108
+ """
109
+ Initialize the analyzer.
110
+
111
+ Args:
112
+ metrics: TokenMetrics instance to use (creates one if not provided)
113
+ """
114
+ self.metrics = metrics or TokenMetrics()
115
+
116
+ # =========================================================================
117
+ # Session Analysis
118
+ # =========================================================================
119
+
120
+ def analyze_session(self, session_id: Optional[str] = None) -> Optional[SessionAnalysis]:
121
+ """
122
+ Perform comprehensive analysis of a session.
123
+
124
+ Args:
125
+ session_id: Session to analyze (defaults to current)
126
+
127
+ Returns:
128
+ SessionAnalysis with all metrics and patterns
129
+ """
130
+ summary = self.metrics.get_session_summary(session_id)
131
+ if not summary:
132
+ return None
133
+
134
+ # Calculate context percentage
135
+ context_percent = (summary.total_tokens / EFFECTIVE_LIMIT_TOKENS) * 100
136
+
137
+ # Build detailed breakdown by type
138
+ breakdown = self._build_type_breakdown(summary)
139
+
140
+ # Detect waste patterns
141
+ waste_patterns = self.detect_waste_patterns(summary.session_id)
142
+
143
+ # Calculate efficiency score
144
+ efficiency_score = self._calculate_efficiency_score(summary, waste_patterns)
145
+
146
+ return SessionAnalysis(
147
+ session_id=summary.session_id,
148
+ summary=summary,
149
+ context_percent_used=round(context_percent, 1),
150
+ breakdown_by_type=breakdown,
151
+ top_consumers=summary.top_consumers,
152
+ waste_patterns=waste_patterns,
153
+ efficiency_score=efficiency_score
154
+ )
155
+
156
+ def analyze_by_component_type(
157
+ self,
158
+ component_type: str,
159
+ session_id: Optional[str] = None
160
+ ) -> Dict[str, Any]:
161
+ """
162
+ Analyze token consumption for a specific component type.
163
+
164
+ Args:
165
+ component_type: Type to analyze (command, hook, tool, etc.)
166
+ session_id: Session to analyze
167
+
168
+ Returns:
169
+ Dict with type-specific metrics
170
+ """
171
+ sid = session_id or self.metrics.current_session_id
172
+ if not sid:
173
+ return {"error": "No active session"}
174
+
175
+ with self.metrics._get_connection() as conn:
176
+ cursor = conn.cursor()
177
+
178
+ # Get all loads of this type
179
+ cursor.execute("""
180
+ SELECT component_name, tokens, source, was_used, loaded_at
181
+ FROM component_loads
182
+ WHERE session_id = ? AND component_type = ?
183
+ ORDER BY tokens DESC
184
+ """, (sid, component_type))
185
+
186
+ components = []
187
+ total_tokens = 0
188
+ used_count = 0
189
+
190
+ for row in cursor.fetchall():
191
+ components.append({
192
+ "name": row["component_name"],
193
+ "tokens": row["tokens"],
194
+ "source": row["source"],
195
+ "was_used": bool(row["was_used"]),
196
+ "loaded_at": row["loaded_at"]
197
+ })
198
+ total_tokens += row["tokens"]
199
+ if row["was_used"]:
200
+ used_count += 1
201
+
202
+ utilization = (used_count / len(components) * 100) if components else 0
203
+
204
+ return {
205
+ "component_type": component_type,
206
+ "total_tokens": total_tokens,
207
+ "component_count": len(components),
208
+ "used_count": used_count,
209
+ "utilization_percent": round(utilization, 1),
210
+ "components": components
211
+ }
212
+
213
+ def _build_type_breakdown(self, summary: SessionSummary) -> Dict[str, Dict[str, Any]]:
214
+ """Build detailed breakdown for each component type."""
215
+ breakdown = {}
216
+ total = summary.total_tokens or 1 # Avoid division by zero
217
+
218
+ for comp_type, tokens in summary.component_breakdown.items():
219
+ percent = (tokens / total) * 100
220
+ breakdown[comp_type] = {
221
+ "tokens": tokens,
222
+ "percent": round(percent, 1),
223
+ "count": self._count_components_of_type(
224
+ summary.session_id, comp_type
225
+ )
226
+ }
227
+
228
+ # Add tool calls as a type
229
+ if summary.tool_tokens > 0:
230
+ breakdown["tools"] = {
231
+ "tokens": summary.tool_tokens,
232
+ "percent": round((summary.tool_tokens / total) * 100, 1),
233
+ "count": summary.tool_calls_count
234
+ }
235
+
236
+ return breakdown
237
+
238
+ def _count_components_of_type(self, session_id: str, comp_type: str) -> int:
239
+ """Count unique components of a type in a session."""
240
+ with self.metrics._get_connection() as conn:
241
+ cursor = conn.cursor()
242
+ cursor.execute("""
243
+ SELECT COUNT(DISTINCT component_name)
244
+ FROM component_loads
245
+ WHERE session_id = ? AND component_type = ?
246
+ """, (session_id, comp_type))
247
+ return cursor.fetchone()[0]
248
+
249
+ # =========================================================================
250
+ # Waste Detection
251
+ # =========================================================================
252
+
253
+ def detect_waste_patterns(self, session_id: Optional[str] = None) -> List[WastePattern]:
254
+ """
255
+ Detect inefficiency patterns in a session.
256
+
257
+ Patterns detected:
258
+ - Redundant loads: Same component loaded multiple times
259
+ - Unused components: Loaded but never used
260
+ - High-cost low-value: Large components with low utilization
261
+
262
+ Args:
263
+ session_id: Session to analyze
264
+
265
+ Returns:
266
+ List of detected waste patterns
267
+ """
268
+ sid = session_id or self.metrics.current_session_id
269
+ if not sid:
270
+ return []
271
+
272
+ patterns = []
273
+
274
+ # Detect redundant loads
275
+ patterns.extend(self._detect_redundant_loads(sid))
276
+
277
+ # Detect unused components
278
+ patterns.extend(self._detect_unused_components(sid))
279
+
280
+ # Detect high-cost low-value
281
+ patterns.extend(self._detect_high_cost_low_value(sid))
282
+
283
+ # Sort by tokens wasted (highest first)
284
+ patterns.sort(key=lambda p: p.tokens_wasted, reverse=True)
285
+
286
+ return patterns
287
+
288
+ def _detect_redundant_loads(self, session_id: str) -> List[WastePattern]:
289
+ """Find components loaded multiple times in same session."""
290
+ patterns = []
291
+
292
+ with self.metrics._get_connection() as conn:
293
+ cursor = conn.cursor()
294
+ cursor.execute("""
295
+ SELECT component_type, component_name,
296
+ COUNT(*) as load_count, SUM(tokens) as total_tokens
297
+ FROM component_loads
298
+ WHERE session_id = ?
299
+ GROUP BY component_type, component_name
300
+ HAVING COUNT(*) > 1
301
+ """, (session_id,))
302
+
303
+ for row in cursor.fetchall():
304
+ # Wasted = total - (one load's worth)
305
+ avg_tokens = row["total_tokens"] // row["load_count"]
306
+ wasted = row["total_tokens"] - avg_tokens
307
+
308
+ patterns.append(WastePattern(
309
+ pattern_type="redundant_load",
310
+ component_name=row["component_name"],
311
+ component_type=row["component_type"],
312
+ tokens_wasted=wasted,
313
+ occurrences=row["load_count"],
314
+ description=f"Loaded {row['load_count']} times, costing {wasted:,} extra tokens"
315
+ ))
316
+
317
+ return patterns
318
+
319
+ def _detect_unused_components(self, session_id: str) -> List[WastePattern]:
320
+ """Find components that were loaded but never used."""
321
+ patterns = []
322
+
323
+ with self.metrics._get_connection() as conn:
324
+ cursor = conn.cursor()
325
+ cursor.execute("""
326
+ SELECT component_type, component_name, SUM(tokens) as total_tokens
327
+ FROM component_loads
328
+ WHERE session_id = ? AND was_used = FALSE
329
+ GROUP BY component_type, component_name
330
+ """, (session_id,))
331
+
332
+ for row in cursor.fetchall():
333
+ # Skip execution-value components (hooks, context)
334
+ # These provide value through running, not through being referenced
335
+ if row["component_type"] in EXECUTION_VALUE_TYPES:
336
+ continue
337
+
338
+ patterns.append(WastePattern(
339
+ pattern_type="unused_component",
340
+ component_name=row["component_name"],
341
+ component_type=row["component_type"],
342
+ tokens_wasted=row["total_tokens"],
343
+ occurrences=1,
344
+ description=f"Loaded but never used, wasting {row['total_tokens']:,} tokens"
345
+ ))
346
+
347
+ return patterns
348
+
349
+ def _detect_high_cost_low_value(self, session_id: str) -> List[WastePattern]:
350
+ """Find large components with low utilization (historical analysis)."""
351
+ patterns = []
352
+
353
+ # Get component stats over 30 days
354
+ stats = self.metrics.get_component_stats(days=30)
355
+
356
+ for stat in stats:
357
+ # Skip if not enough data
358
+ if stat["load_count"] < 5:
359
+ continue
360
+
361
+ # Calculate utilization rate
362
+ utilization = stat["used_count"] / stat["load_count"]
363
+
364
+ # Flag if utilization < 30% and avg tokens > 500
365
+ if utilization < 0.30 and stat["avg_tokens"] > 500:
366
+ potential_waste = int(stat["avg_tokens"] * (1 - utilization))
367
+
368
+ patterns.append(WastePattern(
369
+ pattern_type="high_cost_low_value",
370
+ component_name=stat["component_name"],
371
+ component_type=stat["component_type"],
372
+ tokens_wasted=potential_waste,
373
+ occurrences=stat["load_count"],
374
+ description=f"Only used {utilization*100:.0f}% of the time, avg {stat['avg_tokens']:.0f} tokens"
375
+ ))
376
+
377
+ return patterns
378
+
379
+ # =========================================================================
380
+ # Recommendations
381
+ # =========================================================================
382
+
383
+ def generate_recommendations(
384
+ self,
385
+ analysis: Optional[SessionAnalysis] = None
386
+ ) -> List[Recommendation]:
387
+ """
388
+ Generate actionable optimization recommendations.
389
+
390
+ Args:
391
+ analysis: SessionAnalysis to base recommendations on
392
+
393
+ Returns:
394
+ List of prioritized recommendations
395
+ """
396
+ if analysis is None:
397
+ analysis = self.analyze_session()
398
+
399
+ if analysis is None:
400
+ return []
401
+
402
+ recommendations = []
403
+
404
+ # Add recommendations based on waste patterns
405
+ for pattern in analysis.waste_patterns:
406
+ rec = self._recommendation_for_pattern(pattern)
407
+ if rec:
408
+ recommendations.append(rec)
409
+
410
+ # Add recommendations based on context size
411
+ if analysis.context_percent_used > 60:
412
+ recommendations.append(Recommendation(
413
+ priority=1 if analysis.context_percent_used > 80 else 2,
414
+ category="reduce",
415
+ title="High context utilization",
416
+ description=f"Context is {analysis.context_percent_used:.0f}% full. "
417
+ f"Consider running /clear or compacting soon.",
418
+ estimated_savings=int(analysis.summary.total_tokens * 0.3),
419
+ action="Run /clear to reset context, or compact conversation"
420
+ ))
421
+
422
+ # Add recommendations based on tool usage
423
+ if analysis.summary.tool_tokens > 50000:
424
+ recommendations.append(Recommendation(
425
+ priority=2,
426
+ category="optimize",
427
+ title="High tool token consumption",
428
+ description=f"Tools have consumed {analysis.summary.tool_tokens:,} tokens. "
429
+ f"Consider batching operations.",
430
+ estimated_savings=int(analysis.summary.tool_tokens * 0.2),
431
+ action="Batch file reads and writes when possible"
432
+ ))
433
+
434
+ # Sort by priority then estimated savings
435
+ recommendations.sort(key=lambda r: (r.priority, -r.estimated_savings))
436
+
437
+ return recommendations
438
+
439
+ def _recommendation_for_pattern(self, pattern: WastePattern) -> Optional[Recommendation]:
440
+ """Generate a recommendation for a waste pattern."""
441
+ if pattern.pattern_type == "redundant_load":
442
+ return Recommendation(
443
+ priority=1,
444
+ category="reduce",
445
+ title=f"Remove redundant loads of {pattern.component_name}",
446
+ description=pattern.description,
447
+ estimated_savings=pattern.tokens_wasted,
448
+ action=f"Ensure {pattern.component_name} is only loaded once per session"
449
+ )
450
+
451
+ elif pattern.pattern_type == "unused_component":
452
+ return Recommendation(
453
+ priority=2,
454
+ category="defer",
455
+ title=f"Defer loading {pattern.component_name}",
456
+ description=pattern.description,
457
+ estimated_savings=pattern.tokens_wasted,
458
+ action=f"Load {pattern.component_name} on-demand instead of at startup"
459
+ )
460
+
461
+ elif pattern.pattern_type == "high_cost_low_value":
462
+ return Recommendation(
463
+ priority=3,
464
+ category="optimize",
465
+ title=f"Optimize {pattern.component_name}",
466
+ description=pattern.description,
467
+ estimated_savings=pattern.tokens_wasted,
468
+ action=f"Consider making {pattern.component_name} smaller or on-demand"
469
+ )
470
+
471
+ return None
472
+
473
+ # =========================================================================
474
+ # Efficiency Scoring
475
+ # =========================================================================
476
+
477
+ def _calculate_efficiency_score(
478
+ self,
479
+ summary: SessionSummary,
480
+ waste_patterns: List[WastePattern]
481
+ ) -> float:
482
+ """
483
+ Calculate an efficiency score for the session.
484
+
485
+ Score is 0-100 based on:
486
+ - Context utilization (not too high or too low)
487
+ - Waste as percentage of total tokens
488
+ - Tool efficiency (reasonable input/output ratios)
489
+
490
+ Returns:
491
+ Efficiency score 0-100
492
+ """
493
+ score = 100.0
494
+
495
+ # Penalize for waste
496
+ total_waste = sum(p.tokens_wasted for p in waste_patterns)
497
+ if summary.total_tokens > 0:
498
+ waste_percent = (total_waste / summary.total_tokens) * 100
499
+ # Deduct up to 40 points for waste
500
+ score -= min(40, waste_percent * 2)
501
+
502
+ # Penalize for very high context usage (risk of truncation)
503
+ context_percent = (summary.total_tokens / EFFECTIVE_LIMIT_TOKENS) * 100
504
+ if context_percent > 80:
505
+ score -= (context_percent - 80) # Deduct 1 point per % over 80
506
+
507
+ # Penalize for redundant loads (extra penalty on top of waste)
508
+ redundant_count = sum(
509
+ 1 for p in waste_patterns if p.pattern_type == "redundant_load"
510
+ )
511
+ score -= redundant_count * 2 # 2 points per redundant pattern
512
+
513
+ return max(0, min(100, round(score, 1)))
514
+
515
+ # =========================================================================
516
+ # Historical Efficiency Analysis (Phase 3)
517
+ # =========================================================================
518
+
519
+ def generate_efficiency_report(
520
+ self,
521
+ period_days: int = 7,
522
+ compare_to_previous: bool = True
523
+ ) -> Optional[EfficiencyReport]:
524
+ """
525
+ Generate a historical efficiency report for the specified period.
526
+
527
+ Args:
528
+ period_days: Number of days to analyze (default 7 for weekly)
529
+ compare_to_previous: Whether to compare to the previous period
530
+
531
+ Returns:
532
+ EfficiencyReport with component scores and recommendations
533
+ """
534
+ # Get component stats for the period
535
+ stats = self.metrics.get_component_stats(days=period_days)
536
+
537
+ if not stats:
538
+ return None
539
+
540
+ # Get session history for the period
541
+ sessions = self.metrics.get_session_history(days=period_days)
542
+ if not sessions:
543
+ return None
544
+
545
+ # Calculate component efficiency scores
546
+ component_scores = [
547
+ self._calculate_component_efficiency(stat)
548
+ for stat in stats
549
+ ]
550
+
551
+ # Sort by efficiency score (lowest first to highlight problem areas)
552
+ component_scores.sort(key=lambda c: c.efficiency_score)
553
+
554
+ # Detect trends
555
+ trends = self._detect_component_trends(period_days)
556
+
557
+ # Update component scores with trends
558
+ for comp in component_scores:
559
+ comp_key = f"{comp.component_type}:{comp.component_name}"
560
+ if comp_key in trends:
561
+ comp.trend = trends[comp_key]
562
+
563
+ # Calculate overall metrics
564
+ total_tokens = sum(s.get("total_tokens", 0) for s in sessions)
565
+ avg_tokens = total_tokens / len(sessions) if sessions else 0
566
+
567
+ # Calculate overall efficiency score
568
+ if component_scores:
569
+ overall_score = sum(c.efficiency_score for c in component_scores) / len(component_scores)
570
+ else:
571
+ overall_score = 100.0
572
+
573
+ # Generate recommendations based on component scores
574
+ recommendations = self._generate_efficiency_recommendations(component_scores)
575
+
576
+ # Compare to previous period
577
+ comparison = None
578
+ if compare_to_previous:
579
+ comparison = self._compare_to_previous_period(period_days, total_tokens, overall_score)
580
+
581
+ return EfficiencyReport(
582
+ period_days=period_days,
583
+ generated_at=datetime.now(),
584
+ total_sessions=len(sessions),
585
+ total_tokens=total_tokens,
586
+ avg_tokens_per_session=round(avg_tokens, 0),
587
+ overall_efficiency_score=round(overall_score, 1),
588
+ component_scores=component_scores,
589
+ trends=trends,
590
+ recommendations=recommendations,
591
+ comparison_to_previous=comparison
592
+ )
593
+
594
+ def _calculate_component_efficiency(self, stat: Dict[str, Any]) -> ComponentEfficiency:
595
+ """
596
+ Calculate efficiency score for a single component.
597
+
598
+ Score based on:
599
+ - Utilization (50 points): % of loads where component was used
600
+ - Token cost (30 points): Lower avg tokens = higher score
601
+ - Consistency (20 points): Frequent use with high utilization
602
+
603
+ Args:
604
+ stat: Component statistics dict from get_component_stats
605
+
606
+ Returns:
607
+ ComponentEfficiency with calculated score
608
+ """
609
+ load_count = stat.get("load_count", 0)
610
+ used_count = stat.get("used_count", 0)
611
+ avg_tokens = stat.get("avg_tokens", 0)
612
+ total_tokens = stat.get("total_tokens", 0)
613
+
614
+ # Calculate utilization rate
615
+ utilization = used_count / load_count if load_count > 0 else 0
616
+
617
+ # Utilization score (0-50 points)
618
+ utilization_score = utilization * 50
619
+
620
+ # Token cost score (0-30 points)
621
+ # Score higher for smaller components
622
+ if avg_tokens < 200:
623
+ token_score = 30
624
+ elif avg_tokens < 500:
625
+ token_score = 25
626
+ elif avg_tokens < 1000:
627
+ token_score = 20
628
+ elif avg_tokens < 2000:
629
+ token_score = 15
630
+ elif avg_tokens < 5000:
631
+ token_score = 10
632
+ else:
633
+ token_score = 5
634
+
635
+ # Consistency score (0-20 points)
636
+ # High utilization with frequent use = consistent value
637
+ if load_count >= 10 and utilization >= 0.8:
638
+ consistency_score = 20
639
+ elif load_count >= 5 and utilization >= 0.6:
640
+ consistency_score = 15
641
+ elif load_count >= 3 and utilization >= 0.4:
642
+ consistency_score = 10
643
+ else:
644
+ consistency_score = 5
645
+
646
+ efficiency_score = utilization_score + token_score + consistency_score
647
+
648
+ return ComponentEfficiency(
649
+ component_name=stat.get("component_name", "unknown"),
650
+ component_type=stat.get("component_type", "unknown"),
651
+ total_tokens=total_tokens,
652
+ load_count=load_count,
653
+ used_count=used_count,
654
+ utilization_rate=round(utilization, 3),
655
+ efficiency_score=round(efficiency_score, 1),
656
+ avg_tokens_per_load=round(avg_tokens, 0),
657
+ trend="new" # Will be updated by detect_component_trends
658
+ )
659
+
660
+ def _detect_component_trends(self, period_days: int) -> Dict[str, str]:
661
+ """
662
+ Detect trends by comparing current period to previous period.
663
+
664
+ Args:
665
+ period_days: Length of current period in days
666
+
667
+ Returns:
668
+ Dict mapping component key to trend (improving/stable/degrading/new)
669
+ """
670
+ trends = {}
671
+
672
+ # Get current period stats
673
+ current_stats = self.metrics.get_component_stats(days=period_days)
674
+
675
+ # Get previous period stats (same length, offset by current period)
676
+ previous_stats = self.metrics.get_component_stats(
677
+ days=period_days,
678
+ offset_days=period_days
679
+ )
680
+
681
+ # Build lookup for previous period
682
+ previous_lookup = {}
683
+ for stat in previous_stats:
684
+ key = f"{stat['component_type']}:{stat['component_name']}"
685
+ previous_lookup[key] = stat
686
+
687
+ # Compare each current component to previous
688
+ for stat in current_stats:
689
+ key = f"{stat['component_type']}:{stat['component_name']}"
690
+
691
+ if key not in previous_lookup:
692
+ trends[key] = "new"
693
+ continue
694
+
695
+ prev = previous_lookup[key]
696
+
697
+ # Compare utilization rates
698
+ curr_util = stat.get("used_count", 0) / stat.get("load_count", 1)
699
+ prev_util = prev.get("used_count", 0) / prev.get("load_count", 1)
700
+
701
+ # Determine trend based on utilization change
702
+ util_change = curr_util - prev_util
703
+
704
+ if util_change > 0.1: # More than 10% improvement
705
+ trends[key] = "improving"
706
+ elif util_change < -0.1: # More than 10% decline
707
+ trends[key] = "degrading"
708
+ else:
709
+ trends[key] = "stable"
710
+
711
+ return trends
712
+
713
+ def _generate_efficiency_recommendations(
714
+ self,
715
+ component_scores: List[ComponentEfficiency]
716
+ ) -> List[Recommendation]:
717
+ """
718
+ Generate recommendations based on component efficiency scores.
719
+
720
+ Args:
721
+ component_scores: List of component efficiency scores
722
+
723
+ Returns:
724
+ List of prioritized recommendations
725
+ """
726
+ recommendations = []
727
+
728
+ for comp in component_scores:
729
+ # Skip components with good efficiency
730
+ if comp.efficiency_score >= 70:
731
+ continue
732
+
733
+ # Skip execution-value components (hooks, context)
734
+ # These provide value through running, not through being referenced
735
+ if comp.component_type in EXECUTION_VALUE_TYPES:
736
+ continue
737
+
738
+ # Low utilization = defer loading
739
+ if comp.utilization_rate < 0.3:
740
+ potential_savings = int(comp.avg_tokens_per_load * (1 - comp.utilization_rate))
741
+ recommendations.append(Recommendation(
742
+ priority=1 if comp.efficiency_score < 40 else 2,
743
+ category="defer",
744
+ title=f"Defer loading {comp.component_name}",
745
+ description=f"Used only {comp.utilization_rate*100:.0f}% of the time, "
746
+ f"avg {comp.avg_tokens_per_load:.0f} tokens per load",
747
+ estimated_savings=potential_savings,
748
+ action=f"Move {comp.component_name} to on-demand loading via trigger table"
749
+ ))
750
+
751
+ # High cost = optimize
752
+ elif comp.avg_tokens_per_load > 2000:
753
+ potential_savings = int(comp.avg_tokens_per_load * 0.3) # Assume 30% reduction possible
754
+ recommendations.append(Recommendation(
755
+ priority=2,
756
+ category="optimize",
757
+ title=f"Optimize {comp.component_name} size",
758
+ description=f"Averaging {comp.avg_tokens_per_load:.0f} tokens per load. "
759
+ f"Consider splitting or reducing content.",
760
+ estimated_savings=potential_savings,
761
+ action=f"Review and condense {comp.component_name} content"
762
+ ))
763
+
764
+ # Degrading trend = review
765
+ if comp.trend == "degrading":
766
+ recommendations.append(Recommendation(
767
+ priority=3,
768
+ category="review",
769
+ title=f"Review declining {comp.component_name}",
770
+ description="Utilization is decreasing. May no longer be needed.",
771
+ estimated_savings=int(comp.total_tokens * 0.5),
772
+ action=f"Evaluate if {comp.component_name} is still required"
773
+ ))
774
+
775
+ # Sort by estimated savings first (highest savings first), then priority
776
+ recommendations.sort(key=lambda r: (-r.estimated_savings, r.priority))
777
+
778
+ return recommendations
779
+
780
+ def _compare_to_previous_period(
781
+ self,
782
+ period_days: int,
783
+ current_total_tokens: int,
784
+ current_efficiency_score: float
785
+ ) -> Dict[str, float]:
786
+ """
787
+ Compare current metrics to the previous period.
788
+
789
+ Args:
790
+ period_days: Length of period in days
791
+ current_total_tokens: Total tokens in current period
792
+ current_efficiency_score: Overall efficiency score for current period
793
+
794
+ Returns:
795
+ Dict with comparison metrics (change percentages)
796
+ """
797
+ # Get previous period sessions (same length, offset by current period)
798
+ sessions = self.metrics.get_session_history(
799
+ days=period_days,
800
+ offset_days=period_days
801
+ )
802
+
803
+ if not sessions:
804
+ return {"data_available": False}
805
+
806
+ prev_total_tokens = sum(s.get("total_tokens", 0) for s in sessions)
807
+
808
+ # Calculate previous period efficiency from session scores
809
+ prev_efficiency_scores = [s.get("efficiency_score", 0) for s in sessions if s.get("efficiency_score")]
810
+ prev_efficiency = sum(prev_efficiency_scores) / len(prev_efficiency_scores) if prev_efficiency_scores else 0
811
+
812
+ # Calculate changes
813
+ token_change = 0
814
+ if prev_total_tokens > 0:
815
+ token_change = ((current_total_tokens - prev_total_tokens) / prev_total_tokens) * 100
816
+
817
+ efficiency_change = 0
818
+ if prev_efficiency > 0:
819
+ efficiency_change = current_efficiency_score - prev_efficiency
820
+
821
+ return {
822
+ "token_change_percent": round(token_change, 1),
823
+ "previous_total_tokens": prev_total_tokens,
824
+ "sessions_in_previous": len(sessions),
825
+ "previous_efficiency_score": round(prev_efficiency, 1),
826
+ "efficiency_change_percent": round(efficiency_change, 1)
827
+ }
828
+
829
+ # =========================================================================
830
+ # Formatting Helpers
831
+ # =========================================================================
832
+
833
+ def format_analysis_report(self, analysis: Optional[SessionAnalysis]) -> str:
834
+ """
835
+ Format an analysis as a readable report.
836
+
837
+ Args:
838
+ analysis: SessionAnalysis to format (can be None)
839
+
840
+ Returns:
841
+ Formatted markdown report
842
+ """
843
+ if analysis is None:
844
+ logger.debug("format_analysis_report called with None analysis - no session data available")
845
+ return (
846
+ "## Token Audit Report\n\n"
847
+ "**Status**: No session data available\n\n"
848
+ "The current session has no recorded token metrics. This can happen when:\n"
849
+ "- The session just started and no tool calls have been tracked yet\n"
850
+ "- Token tracking instrumentation is not fully configured\n"
851
+ "- The session ID could not be resolved\n\n"
852
+ "Try making some tool calls first, then run `/audit` again."
853
+ )
854
+
855
+ lines = [
856
+ "## Token Audit Report",
857
+ "",
858
+ f"**Session**: `{analysis.session_id[:8]}...`",
859
+ f"**Analyzed**: {analysis.analyzed_at.strftime('%Y-%m-%d %H:%M')}",
860
+ f"**Efficiency Score**: {analysis.efficiency_score}/100",
861
+ "",
862
+ "### Context Usage",
863
+ "",
864
+ f"- **Total Tokens**: {analysis.summary.total_tokens:,}",
865
+ f"- **Context Used**: {analysis.context_percent_used:.1f}% of effective limit",
866
+ f"- **Peak Tokens**: {analysis.summary.peak_context_tokens:,}",
867
+ "",
868
+ ]
869
+
870
+ # Budget status
871
+ if analysis.summary.budget_tokens:
872
+ lines.extend([
873
+ f"- **Budget**: {analysis.summary.budget_tokens:,} tokens",
874
+ f"- **Budget Used**: {analysis.summary.budget_percent_used:.1f}%",
875
+ "",
876
+ ])
877
+
878
+ # Breakdown by type
879
+ lines.extend([
880
+ "### Breakdown by Type",
881
+ "",
882
+ "| Type | Tokens | % of Total | Count |",
883
+ "|------|--------|------------|-------|",
884
+ ])
885
+
886
+ for comp_type, data in sorted(
887
+ analysis.breakdown_by_type.items(),
888
+ key=lambda x: x[1]["tokens"],
889
+ reverse=True
890
+ ):
891
+ lines.append(
892
+ f"| {comp_type} | {data['tokens']:,} | {data['percent']:.1f}% | {data['count']} |"
893
+ )
894
+
895
+ lines.append("")
896
+
897
+ # Top consumers
898
+ if analysis.top_consumers:
899
+ lines.extend([
900
+ "### Top Token Consumers",
901
+ "",
902
+ "| Component | Type | Tokens |",
903
+ "|-----------|------|--------|",
904
+ ])
905
+
906
+ for consumer in analysis.top_consumers[:5]:
907
+ lines.append(
908
+ f"| {consumer['name']} | {consumer['type']} | {consumer['tokens']:,} |"
909
+ )
910
+
911
+ lines.append("")
912
+
913
+ # Waste patterns
914
+ if analysis.waste_patterns:
915
+ lines.extend([
916
+ "### Detected Waste Patterns",
917
+ "",
918
+ ])
919
+
920
+ for pattern in analysis.waste_patterns[:5]:
921
+ icon = {
922
+ "redundant_load": "🔄",
923
+ "unused_component": "⚠️",
924
+ "high_cost_low_value": "📉"
925
+ }.get(pattern.pattern_type, "•")
926
+
927
+ lines.append(f"- {icon} **{pattern.component_name}**: {pattern.description}")
928
+
929
+ lines.append("")
930
+
931
+ return "\n".join(lines)
932
+
933
+ def format_recommendations(self, recommendations: List[Recommendation]) -> str:
934
+ """
935
+ Format recommendations as a readable list.
936
+
937
+ Args:
938
+ recommendations: List of recommendations to format
939
+
940
+ Returns:
941
+ Formatted markdown recommendations
942
+ """
943
+ if not recommendations:
944
+ return "No recommendations at this time."
945
+
946
+ lines = [
947
+ "### Recommendations",
948
+ "",
949
+ ]
950
+
951
+ priority_icons = {1: "🔴", 2: "🟡", 3: "🟢"}
952
+
953
+ for rec in recommendations:
954
+ icon = priority_icons.get(rec.priority, "•")
955
+ lines.extend([
956
+ f"#### {icon} {rec.title}",
957
+ "",
958
+ f"{rec.description}",
959
+ "",
960
+ f"**Estimated Savings**: ~{rec.estimated_savings:,} tokens",
961
+ ])
962
+
963
+ if rec.action:
964
+ lines.append(f"**Action**: {rec.action}")
965
+
966
+ lines.append("")
967
+
968
+ return "\n".join(lines)
969
+
970
+ def format_efficiency_report(self, report: EfficiencyReport) -> str:
971
+ """
972
+ Format an efficiency report as a readable markdown report.
973
+
974
+ Args:
975
+ report: EfficiencyReport to format
976
+
977
+ Returns:
978
+ Formatted markdown report
979
+ """
980
+ period_label = "Weekly" if report.period_days == 7 else f"{report.period_days}-Day"
981
+
982
+ lines = [
983
+ f"## {period_label} Efficiency Report",
984
+ "",
985
+ f"**Period**: Last {report.period_days} days",
986
+ f"**Generated**: {report.generated_at.strftime('%Y-%m-%d %H:%M')}",
987
+ f"**Overall Efficiency**: {report.overall_efficiency_score:.0f}/100",
988
+ "",
989
+ "### Summary",
990
+ "",
991
+ f"- **Sessions Analyzed**: {report.total_sessions}",
992
+ f"- **Total Tokens**: {report.total_tokens:,}",
993
+ f"- **Avg per Session**: {report.avg_tokens_per_session:,.0f}",
994
+ "",
995
+ ]
996
+
997
+ # Comparison to previous period
998
+ if report.comparison_to_previous and report.comparison_to_previous.get("data_available", True):
999
+ change = report.comparison_to_previous.get("token_change_percent", 0)
1000
+ change_icon = "📈" if change > 0 else "📉" if change < 0 else "➡️"
1001
+ lines.extend([
1002
+ "### Period Comparison",
1003
+ "",
1004
+ f"- **Token Change**: {change_icon} {change:+.1f}% vs previous {report.period_days} days",
1005
+ "",
1006
+ ])
1007
+
1008
+ # Component efficiency scores table
1009
+ lines.extend([
1010
+ "### Component Efficiency Scores",
1011
+ "",
1012
+ "| Component | Type | Score | Utilization | Trend |",
1013
+ "|-----------|------|-------|-------------|-------|",
1014
+ ])
1015
+
1016
+ trend_icons = {
1017
+ "improving": "↑",
1018
+ "stable": "→",
1019
+ "degrading": "↓",
1020
+ "new": "★"
1021
+ }
1022
+
1023
+ for comp in report.component_scores[:10]: # Top 10 worst performing
1024
+ trend_icon = trend_icons.get(comp.trend, "?")
1025
+ lines.append(
1026
+ f"| {comp.component_name} | {comp.component_type} | "
1027
+ f"{comp.efficiency_score:.0f} | {comp.utilization_rate*100:.0f}% | {trend_icon} |"
1028
+ )
1029
+
1030
+ lines.append("")
1031
+
1032
+ # Recommendations
1033
+ if report.recommendations:
1034
+ lines.extend([
1035
+ "### Top Recommendations",
1036
+ "",
1037
+ ])
1038
+
1039
+ priority_icons = {1: "🔴", 2: "🟡", 3: "🟢"}
1040
+
1041
+ for rec in report.recommendations[:5]: # Top 5 recommendations
1042
+ icon = priority_icons.get(rec.priority, "•")
1043
+ lines.append(
1044
+ f"- {icon} **{rec.title}**: {rec.description}"
1045
+ )
1046
+ lines.append(f" - Potential savings: ~{rec.estimated_savings:,} tokens")
1047
+
1048
+ lines.append("")
1049
+
1050
+ return "\n".join(lines)
1051
+
1052
+ # =========================================================================
1053
+ # Self-Improvement Loop (Phase 6)
1054
+ # =========================================================================
1055
+
1056
+ def analyze_usage_patterns(
1057
+ self,
1058
+ days: int = 30
1059
+ ) -> Dict[str, Any]:
1060
+ """
1061
+ Analyze long-term usage patterns to identify optimization opportunities.
1062
+
1063
+ Args:
1064
+ days: Number of days to analyze (default 30)
1065
+
1066
+ Returns:
1067
+ Dict with usage pattern analysis including:
1068
+ - never_used: Components loaded but never marked as used
1069
+ - rarely_used: Components used < 10% of times loaded
1070
+ - high_cost_low_use: High-token components with low utilization
1071
+ - pruning_candidates: Recommended items to remove
1072
+ """
1073
+ # Get component stats for the period
1074
+ stats = self.metrics.get_component_stats(days=days)
1075
+
1076
+ never_used = []
1077
+ rarely_used = []
1078
+ high_cost_low_use = []
1079
+ pruning_candidates = []
1080
+
1081
+ for stat in stats:
1082
+ load_count = stat.get("load_count", 0)
1083
+ used_count = stat.get("used_count", 0)
1084
+ total_tokens = stat.get("total_tokens", 0)
1085
+ avg_tokens = stat.get("avg_tokens", 0)
1086
+
1087
+ if load_count == 0:
1088
+ continue
1089
+
1090
+ utilization = used_count / load_count
1091
+
1092
+ component_info = {
1093
+ "component_name": stat.get("component_name"),
1094
+ "component_type": stat.get("component_type"),
1095
+ "load_count": load_count,
1096
+ "used_count": used_count,
1097
+ "utilization_rate": round(utilization, 2),
1098
+ "total_tokens": total_tokens,
1099
+ "avg_tokens": avg_tokens
1100
+ }
1101
+
1102
+ # Never used (0% utilization)
1103
+ if used_count == 0:
1104
+ never_used.append(component_info)
1105
+ # High token cost never-used = definite prune candidate
1106
+ if total_tokens > 1000:
1107
+ pruning_candidates.append({
1108
+ **component_info,
1109
+ "reason": "Never used, high token cost",
1110
+ "savings": total_tokens
1111
+ })
1112
+
1113
+ # Rarely used (< 10% utilization)
1114
+ elif utilization < 0.1:
1115
+ rarely_used.append(component_info)
1116
+ # High token cost rarely-used = consider pruning
1117
+ if total_tokens > 2000:
1118
+ pruning_candidates.append({
1119
+ **component_info,
1120
+ "reason": "Rarely used, high token cost",
1121
+ "savings": int(total_tokens * 0.8) # Estimate 80% savings
1122
+ })
1123
+
1124
+ # High cost, low utilization (< 30%, > 500 tokens avg)
1125
+ elif utilization < 0.3 and avg_tokens > 500:
1126
+ high_cost_low_use.append(component_info)
1127
+ pruning_candidates.append({
1128
+ **component_info,
1129
+ "reason": "Low utilization, consider deferring",
1130
+ "savings": int(total_tokens * 0.5) # Defer = 50% savings
1131
+ })
1132
+
1133
+ # Sort pruning candidates by potential savings
1134
+ pruning_candidates.sort(key=lambda x: x["savings"], reverse=True)
1135
+
1136
+ return {
1137
+ "period_days": days,
1138
+ "components_analyzed": len(stats),
1139
+ "never_used": never_used,
1140
+ "rarely_used": rarely_used,
1141
+ "high_cost_low_use": high_cost_low_use,
1142
+ "pruning_candidates": pruning_candidates[:10], # Top 10
1143
+ "total_potential_savings": sum(p["savings"] for p in pruning_candidates)
1144
+ }
1145
+
1146
+ def generate_optimization_suggestions(
1147
+ self,
1148
+ usage_analysis: Optional[Dict[str, Any]] = None
1149
+ ) -> List[Dict[str, Any]]:
1150
+ """
1151
+ Generate specific optimization suggestions based on usage analysis.
1152
+
1153
+ Args:
1154
+ usage_analysis: Result from analyze_usage_patterns() or None to run fresh
1155
+
1156
+ Returns:
1157
+ List of actionable suggestions with estimated savings
1158
+ """
1159
+ if usage_analysis is None:
1160
+ usage_analysis = self.analyze_usage_patterns()
1161
+
1162
+ suggestions = []
1163
+ suggestion_id = 1
1164
+
1165
+ # Suggestions for never-used components
1166
+ for comp in usage_analysis.get("never_used", [])[:5]:
1167
+ suggestions.append({
1168
+ "id": suggestion_id,
1169
+ "type": "remove_unused_pattern",
1170
+ "priority": 1,
1171
+ "title": f"Remove unused {comp['component_type']}: {comp['component_name']}",
1172
+ "description": (
1173
+ f"'{comp['component_name']}' has been loaded {comp['load_count']} times "
1174
+ f"but never used. Total cost: {comp['total_tokens']:,} tokens."
1175
+ ),
1176
+ "estimated_savings": comp["total_tokens"],
1177
+ "target_files": self._get_component_files(
1178
+ comp["component_type"],
1179
+ comp["component_name"]
1180
+ ),
1181
+ "changes": {
1182
+ "action": "remove",
1183
+ "component_type": comp["component_type"],
1184
+ "component_name": comp["component_name"]
1185
+ },
1186
+ "risk_level": "low",
1187
+ "reversible": True
1188
+ })
1189
+ suggestion_id += 1
1190
+
1191
+ # Suggestions for deferring rarely-used components
1192
+ for comp in usage_analysis.get("high_cost_low_use", [])[:5]:
1193
+ suggestions.append({
1194
+ "id": suggestion_id,
1195
+ "type": "defer_loading",
1196
+ "priority": 2,
1197
+ "title": f"Defer loading of {comp['component_name']}",
1198
+ "description": (
1199
+ f"'{comp['component_name']}' is only used {comp['utilization_rate']*100:.0f}% "
1200
+ f"of the time but costs {comp['avg_tokens']:,} tokens per load. "
1201
+ f"Consider on-demand loading."
1202
+ ),
1203
+ "estimated_savings": int(comp["total_tokens"] * 0.6),
1204
+ "target_files": self._get_component_files(
1205
+ comp["component_type"],
1206
+ comp["component_name"]
1207
+ ),
1208
+ "changes": {
1209
+ "action": "defer",
1210
+ "component_type": comp["component_type"],
1211
+ "component_name": comp["component_name"],
1212
+ "triggers": [comp["component_name"].lower()]
1213
+ },
1214
+ "risk_level": "medium",
1215
+ "reversible": True
1216
+ })
1217
+ suggestion_id += 1
1218
+
1219
+ # Context reduction suggestions based on session analysis
1220
+ recent_sessions = self.metrics.get_session_history(days=7)
1221
+ if recent_sessions:
1222
+ avg_tokens = sum(s.get("total_tokens", 0) for s in recent_sessions) / len(recent_sessions)
1223
+ if avg_tokens > 10000: # High average consumption
1224
+ suggestions.append({
1225
+ "id": suggestion_id,
1226
+ "type": "reduce_context",
1227
+ "priority": 2,
1228
+ "title": "Reduce initial context size",
1229
+ "description": (
1230
+ f"Average session uses {avg_tokens:,.0f} tokens. "
1231
+ f"Consider moving detailed sections to on-demand commands."
1232
+ ),
1233
+ "estimated_savings": int(avg_tokens * 0.3),
1234
+ "target_files": [".claude/CLAUDE.md"],
1235
+ "changes": {
1236
+ "action": "move_to_on_demand",
1237
+ "sections": ["Anti-Patterns", "Project-Learned Patterns"]
1238
+ },
1239
+ "risk_level": "medium",
1240
+ "reversible": True
1241
+ })
1242
+ suggestion_id += 1
1243
+
1244
+ # Sort by priority and savings
1245
+ suggestions.sort(key=lambda s: (s["priority"], -s["estimated_savings"]))
1246
+
1247
+ return suggestions
1248
+
1249
+ def _get_component_files(
1250
+ self,
1251
+ component_type: str,
1252
+ component_name: str
1253
+ ) -> List[str]:
1254
+ """
1255
+ Get the files associated with a component.
1256
+
1257
+ Args:
1258
+ component_type: Type of component (command, hook, etc.)
1259
+ component_name: Name of the component
1260
+
1261
+ Returns:
1262
+ List of file paths that define or use the component
1263
+ """
1264
+ files = []
1265
+
1266
+ if component_type == "command":
1267
+ # Commands are in global/commands/ or .claude/commands/
1268
+ files.extend([
1269
+ f"global/commands/{component_name}.md",
1270
+ f".claude/commands/{component_name}.md"
1271
+ ])
1272
+ elif component_type == "hook":
1273
+ # Hooks are in .claude/hooks/ or project/hooks/
1274
+ files.extend([
1275
+ f".claude/hooks/{component_name}.py",
1276
+ f"project/hooks/{component_name}.py"
1277
+ ])
1278
+ elif component_type == "context":
1279
+ # Context files
1280
+ files.append(".claude/CLAUDE.md")
1281
+
1282
+ # Filter to existing files (would need filesystem access)
1283
+ # For now, return all potential paths
1284
+ return files
1285
+
1286
+ def track_optimization_impact(
1287
+ self,
1288
+ optimization_id: int,
1289
+ before_tokens: int,
1290
+ after_tokens: int
1291
+ ) -> Dict[str, Any]:
1292
+ """
1293
+ Track the impact of an applied optimization.
1294
+
1295
+ Args:
1296
+ optimization_id: ID of the optimization
1297
+ before_tokens: Token count before optimization
1298
+ after_tokens: Token count after optimization
1299
+
1300
+ Returns:
1301
+ Dict with impact metrics
1302
+ """
1303
+ savings = before_tokens - after_tokens
1304
+ savings_percent = (savings / before_tokens * 100) if before_tokens > 0 else 0
1305
+
1306
+ impact = {
1307
+ "optimization_id": optimization_id,
1308
+ "before_tokens": before_tokens,
1309
+ "after_tokens": after_tokens,
1310
+ "tokens_saved": savings,
1311
+ "savings_percent": round(savings_percent, 1),
1312
+ "effective": savings > 0,
1313
+ "timestamp": datetime.now(timezone.utc).isoformat()
1314
+ }
1315
+
1316
+ # If effectiveness is negative, suggest reversal
1317
+ if savings < 0:
1318
+ impact["recommendation"] = "Consider reverting - optimization increased token usage"
1319
+
1320
+ return impact
1321
+
1322
+ def format_suggestions_report(
1323
+ self,
1324
+ suggestions: List[Dict[str, Any]]
1325
+ ) -> str:
1326
+ """
1327
+ Format optimization suggestions as a readable report.
1328
+
1329
+ Args:
1330
+ suggestions: List of suggestions from generate_optimization_suggestions()
1331
+
1332
+ Returns:
1333
+ Formatted markdown report
1334
+ """
1335
+ if not suggestions:
1336
+ return "## Optimization Suggestions\n\n*No optimization opportunities found.*"
1337
+
1338
+ total_savings = sum(s["estimated_savings"] for s in suggestions)
1339
+
1340
+ lines = [
1341
+ "## Optimization Suggestions",
1342
+ "",
1343
+ f"**Total Potential Savings**: ~{total_savings:,} tokens",
1344
+ f"**Suggestions**: {len(suggestions)}",
1345
+ "",
1346
+ "### Recommendations",
1347
+ ""
1348
+ ]
1349
+
1350
+ priority_labels = {1: "High", 2: "Medium", 3: "Low"}
1351
+ risk_emojis = {"low": "🟢", "medium": "🟡", "high": "🔴"}
1352
+
1353
+ for suggestion in suggestions:
1354
+ priority = priority_labels.get(suggestion["priority"], "?")
1355
+ risk = risk_emojis.get(suggestion.get("risk_level", "medium"), "⚪")
1356
+
1357
+ lines.extend([
1358
+ f"#### [{suggestion['id']}] {suggestion['title']}",
1359
+ "",
1360
+ f"**Priority**: {priority} | **Risk**: {risk} | "
1361
+ f"**Savings**: ~{suggestion['estimated_savings']:,} tokens",
1362
+ "",
1363
+ suggestion["description"],
1364
+ "",
1365
+ f"**Apply with**: `/efficiency --apply {suggestion['id']}`",
1366
+ "",
1367
+ "---",
1368
+ ""
1369
+ ])
1370
+
1371
+ return "\n".join(lines)
1372
+
1373
+
1374
+ # Singleton instance for convenience
1375
+ _analyzer_instance: Optional[TokenAnalyzer] = None
1376
+
1377
+
1378
+ def get_analyzer() -> TokenAnalyzer:
1379
+ """Get or create the singleton analyzer instance."""
1380
+ global _analyzer_instance
1381
+ if _analyzer_instance is None:
1382
+ _analyzer_instance = TokenAnalyzer()
1383
+ return _analyzer_instance