empathy-framework 3.9.3__py3-none-any.whl → 3.10.2__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: empathy-framework
3
- Version: 3.9.3
3
+ Version: 3.10.2
4
4
  Summary: AI collaboration framework with intelligent caching (up to 57% cache hit rate), tier routing (34-86% cost savings depending on task complexity), XML-enhanced prompts, persistent memory, CrewAI integration, and multi-agent orchestration. Includes HIPAA-compliant healthcare wizards.
5
5
  Author-email: Patrick Roebuck <admin@smartaimemory.com>
6
6
  Maintainer-email: Smart-AI-Memory <admin@smartaimemory.com>
@@ -178,7 +178,7 @@ Requires-Dist: pyyaml<7.0,>=6.0
178
178
  Requires-Dist: anthropic<1.0.0,>=0.25.0
179
179
  Requires-Dist: crewai<1.0.0,>=0.1.0
180
180
  Requires-Dist: langchain<2.0.0,>=0.1.0
181
- Requires-Dist: langchain-core<2.0.0,>=0.1.0
181
+ Requires-Dist: langchain-core<2.0.0,>=1.2.6
182
182
  Provides-Extra: anthropic
183
183
  Requires-Dist: anthropic<1.0.0,>=0.25.0; extra == "anthropic"
184
184
  Provides-Extra: openai
@@ -368,7 +368,41 @@ Dynamic: license-file
368
368
  pip install empathy-framework[developer] # Lightweight for individual developers
369
369
  ```
370
370
 
371
- ## What's New in v3.9.0 (Current Release)
371
+ ## What's New in v3.10.0 (Current Release)
372
+
373
+ ### 🎯 **Intelligent Tier Fallback: Start CHEAP, Upgrade Only When Needed**
374
+
375
+ **Automatic cost optimization with quality-based tier escalation.**
376
+
377
+ - ✅ **30-50% cost savings** on average workflow execution
378
+ - ✅ **CHEAP → CAPABLE → PREMIUM** automatic fallback chain
379
+ - ✅ **Quality gates** validate each tier before upgrading
380
+ - ✅ **Opt-in design** - backward compatible, enabled via `--use-recommended-tier`
381
+ - ✅ **Full telemetry** tracks tier progression and savings
382
+
383
+ ```bash
384
+ # Enable intelligent tier fallback
385
+ empathy workflow run health-check --use-recommended-tier
386
+
387
+ # Result: Both stages succeeded at CHEAP tier
388
+ # 💰 Cost Savings: $0.0300 (66.7% vs. all-PREMIUM)
389
+ ```
390
+
391
+ **How it works:**
392
+ 1. Try CHEAP tier first (Haiku)
393
+ 2. If quality gates fail → upgrade to CAPABLE (Sonnet 4.5)
394
+ 3. If still failing → upgrade to PREMIUM (Opus 4.5)
395
+ 4. Track savings and learn from patterns
396
+
397
+ **When to use:** Cost-sensitive workflows where quality can be validated (health-check, test-gen, doc-gen)
398
+
399
+ See [CHANGELOG.md](https://github.com/Smart-AI-Memory/empathy-framework/blob/main/CHANGELOG.md#3100---2026-01-09) for full details.
400
+
401
+ ---
402
+
403
+ ### Previous Releases
404
+
405
+ #### v3.9.0
372
406
 
373
407
  ### 🔒 **Security Hardening: 174 Security Tests (Up from 14)**
374
408
 
@@ -407,8 +441,6 @@ See [SECURITY.md](https://github.com/Smart-AI-Memory/empathy-framework/blob/main
407
441
 
408
442
  ---
409
443
 
410
- ### Previous Releases
411
-
412
444
  #### v3.8.3
413
445
 
414
446
  ### 🎯 **Transparent Cost Claims: Honest Role-Based Savings (34-86%)**
@@ -20,7 +20,7 @@ coach_wizards/refactoring_wizard.py,sha256=X0MTx3BHpOlOMAYDow-3HX5GyryY70JGAF5vA
20
20
  coach_wizards/scaling_wizard.py,sha256=n1RLtpWmj1RSEGSWssMiUPwCdpskO3z2Z3yhLlTdXro,2598
21
21
  coach_wizards/security_wizard.py,sha256=19SOClSxo6N-QqUc_QsFXOE7yEquiZF4kLi7jRomA7g,2605
22
22
  coach_wizards/testing_wizard.py,sha256=vKFgFG4uJfAVFmCIQbkrWNvZhIfLC6ve_XbvWZKrPg4,2563
23
- empathy_framework-3.9.3.dist-info/licenses/LICENSE,sha256=IJ9eeI5KSrD5P7alsn7sI_6_1bDihxBA5S4Sen4jf2k,4937
23
+ empathy_framework-3.10.2.dist-info/licenses/LICENSE,sha256=IJ9eeI5KSrD5P7alsn7sI_6_1bDihxBA5S4Sen4jf2k,4937
24
24
  empathy_healthcare_plugin/__init__.py,sha256=4NioL1_86UXzkd-QNkQZUSZ8rKTQGSP0TC9VXP32kQs,295
25
25
  empathy_healthcare_plugin/monitors/__init__.py,sha256=Udp8qfZR504QAq5_eQjvtIaE7v06Yguc7nuF40KllQc,196
26
26
  empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py,sha256=GkNh2Yuw9cvuKuPh3mriWtKJZFq_sTxBD7Ci8lFV9gQ,11620
@@ -95,8 +95,8 @@ empathy_llm_toolkit/wizards/patient_assessment_wizard.py,sha256=dsvoOq0AYCBigmn6
95
95
  empathy_llm_toolkit/wizards/technology_wizard.py,sha256=8hQirzzGQp7UVtj1hFCoaoLLtqAtx9HFf4mdUWV1xH0,7533
96
96
  empathy_os/__init__.py,sha256=zAvie0wQFlSpv7thmNNtdrPy0YY1NUaQfb1oD_hKmns,5896
97
97
  empathy_os/agent_monitoring.py,sha256=s4seLC_J4AtQ3PYWrRPO8YHM-Fbm0Q36kPEdlTHf2HI,13375
98
- empathy_os/cli.py,sha256=wT131LYtNskfOu8thDjrv724UXyjaRUuuFmylZqr_YM,110271
99
- empathy_os/cli_unified.py,sha256=M-fLRibRlsDB1kcJsXOXHMTOF4UDFKWZE9mK1zyU7Hw,26452
98
+ empathy_os/cli.py,sha256=2xeJgb3EZ9PT9Uc-4V6X_8WIcPRlGFklVTf3QkE5yk0,116971
99
+ empathy_os/cli_unified.py,sha256=o6l1nokC5Ku8nvEDAEBJoP_p63g5xbVWz_JKTSFNsgE,27181
100
100
  empathy_os/config.py,sha256=jsFQuXpZP_jt6UrZtfLlkBZwPUSNbnW5VVtMS5JIhbA,16276
101
101
  empathy_os/coordination.py,sha256=E2HvHxKk1xbYswtgxhnVKB6DRxfXUV5pCt-XWHOvNKM,28509
102
102
  empathy_os/core.py,sha256=PvrPs8JQPwQjPZZQj6zy6xQTFGHKziaeRxCIlIs3iHA,52951
@@ -124,7 +124,7 @@ empathy_os/cache/__init__.py,sha256=nKHyWnQmyBU6f24UOq5iZ4ULUFys5Dhl6dFZlU0jlHU,
124
124
  empathy_os/cache/base.py,sha256=1waj2OZ_5gRhwYhSfsSfgQs1N_ftZcAq6UAV9RctSek,3968
125
125
  empathy_os/cache/dependency_manager.py,sha256=Khwpi4LiT5xFp1Pka4XQHO0bLzWUG1ElpRQ_Zmup_iM,7614
126
126
  empathy_os/cache/hash_only.py,sha256=CdyZvl81lbargRyVhWVBJB4VDUNay4KIf3knUmqvZCo,7427
127
- empathy_os/cache/hybrid.py,sha256=d8R-r1IarW5tEKtjyBsFVSFKv_mGmn9jPhRUzqaST6o,12588
127
+ empathy_os/cache/hybrid.py,sha256=HSqStILYY6YhfyKGnYkrjYnGpOjHemMEF7YaKivtbQU,15264
128
128
  empathy_os/cache/storage.py,sha256=VrvvpAeQF29T-d6Ly6L-bqvu-XHFJRWICb-gfiC0Hqw,7659
129
129
  empathy_os/config/__init__.py,sha256=jkdGF6eWEqOayld3Xcl1tsk92YIXIBXI-QcecMUBbXE,1577
130
130
  empathy_os/config/xml_config.py,sha256=QOnFV0rshX-7xoUSadLNDCltz5DZwCi8UvC0y2Pqt-w,8672
@@ -165,7 +165,7 @@ empathy_os/models/fallback.py,sha256=6xerVS4VHaFB9JC0XnPEGHMrYo9CLACYnCx1QPu3zC8
165
165
  empathy_os/models/provider_config.py,sha256=-r-q-Yiy1gjBp3ovP7VXz3bEJ6hjVaoPCw5tzBFpKHQ,19463
166
166
  empathy_os/models/registry.py,sha256=tBLCzjZeb_wuzAPL5aMXI2VIl3jhXWvprc_whfn0ah8,12987
167
167
  empathy_os/models/tasks.py,sha256=XyS0IN6kadLpyC26sMiX9FEXgfP2j57AYPJO2oH4sKU,8691
168
- empathy_os/models/telemetry.py,sha256=BLtmbs90tO59WQhA_AKeL4saQZqbjhVZdfQLJB92oxY,16653
168
+ empathy_os/models/telemetry.py,sha256=bsiUAtI-6zHpTtnWpfHLYgsRuQeF8KmJFJyIlUJfyoM,45558
169
169
  empathy_os/models/token_estimator.py,sha256=hUeWH0oJvp2UbYRLMsgDBGQRzICfLQxzPi_rsqP6fkE,12576
170
170
  empathy_os/models/validation.py,sha256=K7Pki8jKmQ3zKG2CSJtx4Bv7cfn9l5g6aIxXWdMA4cs,8925
171
171
  empathy_os/monitoring/__init__.py,sha256=efAzUX0DYeUTGEES-MV6jI4iUYmhH-RkQYnVLnjSl-M,1507
@@ -237,9 +237,9 @@ empathy_os/workflows/config.py,sha256=qahi2OOs-5ahaZd4LWkH2uX0N8YV9KxY-o6popeS4I
237
237
  empathy_os/workflows/dependency_check.py,sha256=zWwSCAWThnAUkOqEUx2GTl5Vgf2fW9P5HRqwWhYZXlc,23452
238
238
  empathy_os/workflows/document_gen.py,sha256=z9jVtGYCvl01oNk2JfelQs4CfFMw35t1vo9IIeC6sWQ,39135
239
239
  empathy_os/workflows/documentation_orchestrator.py,sha256=sSzTsLJPkSlHcE_o-pfM-27DVAVpwPcmxPZT71GYfeo,43013
240
- empathy_os/workflows/health_check.py,sha256=1yxNPV4wpLNEoMlbTm_S9Dlbp7q4L3WZM5kL_1LKVDQ,24633
240
+ empathy_os/workflows/health_check.py,sha256=0OOfL7OyTfQYL1uspwAdTqMdWZkM6zQauE5Gn2v9qNg,26067
241
241
  empathy_os/workflows/manage_documentation.py,sha256=gknIse4MzTLxRowIAS07WSXNqWAoWCfxmoIJSbTYBNM,29419
242
- empathy_os/workflows/new_sample_workflow1.py,sha256=GBUdgGtjrA1h7Xrdt14GsWjcMYvRMIPgRDM1gZ7Qex0,3978
242
+ empathy_os/workflows/new_sample_workflow1.py,sha256=vG4s_Dnn61y7qe1PEwjIBkKpXh6afpEVXp8orCI_EOo,3977
243
243
  empathy_os/workflows/new_sample_workflow1_README.md,sha256=bzLyqukgaKilG1OnCdLsc5GNWsYEagI7mn3n80BPMHY,2366
244
244
  empathy_os/workflows/perf_audit.py,sha256=JzJjuU5WQAa7Eisdwmwz4CQTB5W5v-mphVJ_yhORM5A,25275
245
245
  empathy_os/workflows/pr_review.py,sha256=H6YukqPrHYA8uDmddISYy0PVp8axmaoeBeB6dZ0GpLE,26193
@@ -334,8 +334,8 @@ workflow_scaffolding/__init__.py,sha256=UpX5vjjjPjIaAKyIV1D4GxJzLUZy5DzdzgSkePYM
334
334
  workflow_scaffolding/__main__.py,sha256=0qspuNoadTDqyskXTlT8Sahqau-XIxN35NHTSGVW6z4,236
335
335
  workflow_scaffolding/cli.py,sha256=R4rCTDENRMil2c3v32MnisqteFRDfilS6RHBNlYV39Q,6752
336
336
  workflow_scaffolding/generator.py,sha256=whWbBmWEA0rN3M3X9EzTjfbwBxHcF40Jin8-nbj0S0E,8858
337
- empathy_framework-3.9.3.dist-info/METADATA,sha256=lY3RB1JHOHUm4wET6B2oqddzH3hD0zcOPzFn0uH5nVM,50099
338
- empathy_framework-3.9.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
339
- empathy_framework-3.9.3.dist-info/entry_points.txt,sha256=zMu7sKCiLndbEEXjTecltS-1P_JZoEUKrifuRBBbroc,1268
340
- empathy_framework-3.9.3.dist-info/top_level.txt,sha256=wrNU1aVMutVDACer58H-udv0P_171Dv6z_42sZtZ-xM,124
341
- empathy_framework-3.9.3.dist-info/RECORD,,
337
+ empathy_framework-3.10.2.dist-info/METADATA,sha256=pQcro0TjRgxx_66gSv4KDQfX61zzBhlNlHsxR9LKu88,51251
338
+ empathy_framework-3.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
339
+ empathy_framework-3.10.2.dist-info/entry_points.txt,sha256=zMu7sKCiLndbEEXjTecltS-1P_JZoEUKrifuRBBbroc,1268
340
+ empathy_framework-3.10.2.dist-info/top_level.txt,sha256=wrNU1aVMutVDACer58H-udv0P_171Dv6z_42sZtZ-xM,124
341
+ empathy_framework-3.10.2.dist-info/RECORD,,
@@ -15,11 +15,13 @@ Licensed under Fair Source License 0.9
15
15
  import hashlib
16
16
  import logging
17
17
  import time
18
+ from pathlib import Path
18
19
  from typing import TYPE_CHECKING, Any
19
20
 
20
21
  import numpy as np
21
22
 
22
23
  from .base import BaseCache, CacheEntry, CacheStats
24
+ from .storage import CacheStorage
23
25
 
24
26
  if TYPE_CHECKING:
25
27
  from sentence_transformers import SentenceTransformer
@@ -77,6 +79,7 @@ class HybridCache(BaseCache):
77
79
  similarity_threshold: float = 0.95,
78
80
  model_name: str = "all-MiniLM-L6-v2",
79
81
  device: str = "cpu",
82
+ cache_dir: Path | None = None,
80
83
  ):
81
84
  """Initialize hybrid cache.
82
85
 
@@ -87,6 +90,7 @@ class HybridCache(BaseCache):
87
90
  similarity_threshold: Semantic similarity threshold (0.0-1.0, default: 0.95).
88
91
  model_name: Sentence transformer model (default: all-MiniLM-L6-v2).
89
92
  device: Device for embeddings ("cpu" or "cuda").
93
+ cache_dir: Directory for persistent cache storage (default: ~/.empathy/cache/).
90
94
 
91
95
  """
92
96
  super().__init__(max_size_mb, default_ttl)
@@ -106,9 +110,16 @@ class HybridCache(BaseCache):
106
110
  self._model: SentenceTransformer | None = None
107
111
  self._load_model()
108
112
 
113
+ # Initialize persistent storage
114
+ self._storage = CacheStorage(cache_dir=cache_dir, max_disk_mb=max_size_mb)
115
+
116
+ # Load existing entries from storage into memory caches
117
+ self._load_from_storage()
118
+
109
119
  logger.info(
110
120
  f"HybridCache initialized (model: {model_name}, threshold: {similarity_threshold}, "
111
- f"device: {device}, max_memory: {max_memory_mb}MB)"
121
+ f"device: {device}, max_memory: {max_memory_mb}MB, "
122
+ f"loaded: {len(self._hash_cache)} entries from disk)"
112
123
  )
113
124
 
114
125
  def _load_model(self) -> None:
@@ -131,6 +142,34 @@ class HybridCache(BaseCache):
131
142
  logger.warning("Falling back to hash-only mode")
132
143
  self._model = None
133
144
 
145
+ def _load_from_storage(self) -> None:
146
+ """Load cached entries from persistent storage into memory caches."""
147
+ try:
148
+ # Get all non-expired entries from storage
149
+ entries = self._storage.get_all()
150
+
151
+ if not entries:
152
+ logger.debug("No cached entries found in storage")
153
+ return
154
+
155
+ # Populate hash cache
156
+ for entry in entries:
157
+ self._hash_cache[entry.key] = entry
158
+ self._access_times[entry.key] = entry.timestamp
159
+
160
+ logger.info(f"Loaded {len(entries)} entries from persistent storage into hash cache")
161
+
162
+ # Populate semantic cache if model available
163
+ if self._model is not None:
164
+ logger.debug("Generating embeddings for cached prompts...")
165
+ # Note: We don't have the original prompts, so semantic cache
166
+ # will be populated on-demand as cache hits occur
167
+ # This is acceptable since semantic matching is secondary to hash matching
168
+ logger.debug("Semantic cache will be populated on-demand from hash hits")
169
+
170
+ except Exception as e:
171
+ logger.warning(f"Failed to load cache from storage: {e}, starting with empty cache")
172
+
134
173
  def get(
135
174
  self,
136
175
  workflow: str,
@@ -257,7 +296,7 @@ class HybridCache(BaseCache):
257
296
  response: Any,
258
297
  ttl: int | None = None,
259
298
  ) -> None:
260
- """Store response in both hash and semantic caches.
299
+ """Store response in both hash and semantic caches, and persist to disk.
261
300
 
262
301
  Args:
263
302
  workflow: Workflow name.
@@ -295,14 +334,26 @@ class HybridCache(BaseCache):
295
334
  prompt_embedding = self._model.encode(prompt, convert_to_numpy=True)
296
335
  self._semantic_cache.append((prompt_embedding, entry))
297
336
 
298
- logger.debug(
299
- f"Cache PUT (hybrid): {workflow}/{stage} "
300
- f"(hash_entries: {len(self._hash_cache)}, "
301
- f"semantic_entries: {len(self._semantic_cache)})"
302
- )
337
+ # Persist to disk storage
338
+ try:
339
+ self._storage.put(entry)
340
+ logger.debug(
341
+ f"Cache PUT (hybrid): {workflow}/{stage} "
342
+ f"(hash_entries: {len(self._hash_cache)}, "
343
+ f"semantic_entries: {len(self._semantic_cache)}, "
344
+ f"persisted: True)"
345
+ )
346
+ except Exception as e:
347
+ logger.warning(f"Failed to persist cache entry to disk: {e}")
348
+ logger.debug(
349
+ f"Cache PUT (hybrid): {workflow}/{stage} "
350
+ f"(hash_entries: {len(self._hash_cache)}, "
351
+ f"semantic_entries: {len(self._semantic_cache)}, "
352
+ f"persisted: False)"
353
+ )
303
354
 
304
355
  def clear(self) -> None:
305
- """Clear all cached entries."""
356
+ """Clear all cached entries from memory and disk."""
306
357
  hash_count = len(self._hash_cache)
307
358
  semantic_count = len(self._semantic_cache)
308
359
 
@@ -310,7 +361,16 @@ class HybridCache(BaseCache):
310
361
  self._access_times.clear()
311
362
  self._semantic_cache.clear()
312
363
 
313
- logger.info(f"Cache cleared (hash: {hash_count}, semantic: {semantic_count} entries)")
364
+ # Clear persistent storage
365
+ try:
366
+ storage_count = self._storage.clear()
367
+ logger.info(
368
+ f"Cache cleared (hash: {hash_count}, semantic: {semantic_count}, "
369
+ f"storage: {storage_count} entries)"
370
+ )
371
+ except Exception as e:
372
+ logger.warning(f"Failed to clear persistent storage: {e}")
373
+ logger.info(f"Cache cleared (hash: {hash_count}, semantic: {semantic_count} entries)")
314
374
 
315
375
  def get_stats(self) -> CacheStats:
316
376
  """Get cache statistics."""
empathy_os/cli.py CHANGED
@@ -41,11 +41,15 @@ from empathy_os.workflows import list_workflows as get_workflow_list
41
41
  # Import telemetry CLI commands
42
42
  try:
43
43
  from empathy_os.telemetry.cli import (
44
+ cmd_agent_performance,
45
+ cmd_task_routing_report,
44
46
  cmd_telemetry_compare,
45
47
  cmd_telemetry_export,
46
48
  cmd_telemetry_reset,
47
49
  cmd_telemetry_savings,
48
50
  cmd_telemetry_show,
51
+ cmd_test_status,
52
+ cmd_tier1_status,
49
53
  )
50
54
 
51
55
  TELEMETRY_CLI_AVAILABLE = True
@@ -2148,7 +2152,20 @@ def cmd_workflow(args):
2148
2152
 
2149
2153
  wf_config = WorkflowConfig.load()
2150
2154
  provider = wf_config.default_provider
2151
- workflow = workflow_cls(provider=provider)
2155
+
2156
+ # Initialize workflow with tier fallback if requested
2157
+ use_tier_fallback = getattr(args, "use_recommended_tier", False)
2158
+ workflow_kwargs = {
2159
+ "provider": provider,
2160
+ "enable_tier_fallback": use_tier_fallback,
2161
+ }
2162
+
2163
+ # Add health-check specific parameters
2164
+ if name == "health-check":
2165
+ health_score_threshold = getattr(args, "health_score_threshold", 100)
2166
+ workflow_kwargs["health_score_threshold"] = health_score_threshold
2167
+
2168
+ workflow = workflow_cls(**workflow_kwargs)
2152
2169
 
2153
2170
  # Parse input
2154
2171
  input_data = {}
@@ -2244,25 +2261,76 @@ def cmd_workflow(args):
2244
2261
  }
2245
2262
  print(json_mod.dumps(output, indent=2))
2246
2263
  # Display the actual results - this is what users want to see
2247
- elif result.success:
2248
- if output_content:
2249
- print(f"\n{output_content}\n")
2250
- else:
2251
- print("\n✓ Workflow completed successfully.\n")
2252
2264
  else:
2253
- # Extract error from various result types
2254
- error_msg = getattr(result, "error", None)
2255
- if not error_msg:
2256
- # Check for blockers (CodeReviewPipelineResult)
2257
- blockers = getattr(result, "blockers", [])
2258
- if blockers:
2259
- error_msg = "; ".join(blockers)
2265
+ # Show tier progression if tier fallback was used
2266
+ if use_tier_fallback and hasattr(workflow, "_tier_progression"):
2267
+ tier_progression = workflow._tier_progression
2268
+ if tier_progression:
2269
+ print("\n" + "=" * 60)
2270
+ print(" TIER PROGRESSION (Intelligent Fallback)")
2271
+ print("=" * 60)
2272
+
2273
+ # Group by stage
2274
+ stage_tiers: dict[str, list[tuple[str, bool]]] = {}
2275
+ for stage, tier, success in tier_progression:
2276
+ if stage not in stage_tiers:
2277
+ stage_tiers[stage] = []
2278
+ stage_tiers[stage].append((tier, success))
2279
+
2280
+ # Display progression for each stage
2281
+ for stage, attempts in stage_tiers.items():
2282
+ status = "✓" if any(success for _, success in attempts) else "✗"
2283
+ print(f"\n{status} Stage: {stage}")
2284
+
2285
+ for idx, (tier, success) in enumerate(attempts, 1):
2286
+ attempt_status = "✓ SUCCESS" if success else "✗ FAILED"
2287
+ if idx == 1:
2288
+ print(f" Attempt {idx}: {tier.upper():8} → {attempt_status}")
2289
+ else:
2290
+ prev_tier = attempts[idx - 2][0]
2291
+ print(
2292
+ f" Attempt {idx}: {tier.upper():8} → {attempt_status} "
2293
+ f"(upgraded from {prev_tier.upper()})"
2294
+ )
2295
+
2296
+ # Calculate cost savings (only if result has stages attribute)
2297
+ if hasattr(result, "stages") and result.stages:
2298
+ actual_cost = sum(stage.cost for stage in result.stages if stage.cost)
2299
+ # Estimate what cost would be if all stages used PREMIUM
2300
+ premium_cost = actual_cost * 3 # Conservative estimate
2301
+
2302
+ savings = premium_cost - actual_cost
2303
+ savings_pct = (savings / premium_cost * 100) if premium_cost > 0 else 0
2304
+
2305
+ print("\n" + "-" * 60)
2306
+ print("💰 Cost Savings:")
2307
+ print(f" Actual cost: ${actual_cost:.4f}")
2308
+ print(f" Premium cost: ${premium_cost:.4f} (if all PREMIUM)")
2309
+ print(f" Savings: ${savings:.4f} ({savings_pct:.1f}%)")
2310
+ print("=" * 60 + "\n")
2311
+
2312
+ # Display workflow result
2313
+ if result.success:
2314
+ if output_content:
2315
+ print(f"\n{output_content}\n")
2260
2316
  else:
2261
- # Check metadata for error
2262
- metadata = getattr(result, "metadata", {})
2263
- error_msg = metadata.get("error") if isinstance(metadata, dict) else None
2264
- error_msg = error_msg or "Unknown error"
2265
- print(f"\n✗ Workflow failed: {error_msg}\n")
2317
+ print("\n✓ Workflow completed successfully.\n")
2318
+ else:
2319
+ # Extract error from various result types
2320
+ error_msg = getattr(result, "error", None)
2321
+ if not error_msg:
2322
+ # Check for blockers (CodeReviewPipelineResult)
2323
+ blockers = getattr(result, "blockers", [])
2324
+ if blockers:
2325
+ error_msg = "; ".join(blockers)
2326
+ else:
2327
+ # Check metadata for error
2328
+ metadata = getattr(result, "metadata", {})
2329
+ error_msg = (
2330
+ metadata.get("error") if isinstance(metadata, dict) else None
2331
+ )
2332
+ error_msg = error_msg or "Unknown error"
2333
+ print(f"\n✗ Workflow failed: {error_msg}\n")
2266
2334
 
2267
2335
  except KeyError as e:
2268
2336
  print(f"Error: {e}")
@@ -2436,6 +2504,38 @@ def _cmd_telemetry_export(args):
2436
2504
  return cmd_telemetry_export(args)
2437
2505
 
2438
2506
 
2507
+ def _cmd_tier1_status(args):
2508
+ """Wrapper for tier1 status command."""
2509
+ if not TELEMETRY_CLI_AVAILABLE:
2510
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2511
+ return 1
2512
+ return cmd_tier1_status(args)
2513
+
2514
+
2515
+ def _cmd_task_routing_report(args):
2516
+ """Wrapper for task routing report command."""
2517
+ if not TELEMETRY_CLI_AVAILABLE:
2518
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2519
+ return 1
2520
+ return cmd_task_routing_report(args)
2521
+
2522
+
2523
+ def _cmd_test_status(args):
2524
+ """Wrapper for test status command."""
2525
+ if not TELEMETRY_CLI_AVAILABLE:
2526
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2527
+ return 1
2528
+ return cmd_test_status(args)
2529
+
2530
+
2531
+ def _cmd_agent_performance(args):
2532
+ """Wrapper for agent performance command."""
2533
+ if not TELEMETRY_CLI_AVAILABLE:
2534
+ print("Tier 1 monitoring commands not available. Install telemetry dependencies.")
2535
+ return 1
2536
+ return cmd_agent_performance(args)
2537
+
2538
+
2439
2539
  def main():
2440
2540
  """Main CLI entry point"""
2441
2541
  # Configure Windows-compatible asyncio event loop policy
@@ -2929,6 +3029,60 @@ def main():
2929
3029
  )
2930
3030
  parser_telemetry_export.set_defaults(func=lambda args: _cmd_telemetry_export(args))
2931
3031
 
3032
+ # Tier 1 automation monitoring commands
3033
+
3034
+ # tier1 command - comprehensive status
3035
+ parser_tier1 = subparsers.add_parser(
3036
+ "tier1",
3037
+ help="Show Tier 1 automation status (tasks, tests, coverage, agents)",
3038
+ )
3039
+ parser_tier1.add_argument(
3040
+ "--hours",
3041
+ type=int,
3042
+ default=24,
3043
+ help="Hours to analyze (default: 24)",
3044
+ )
3045
+ parser_tier1.set_defaults(func=lambda args: _cmd_tier1_status(args))
3046
+
3047
+ # tasks command - task routing report
3048
+ parser_tasks = subparsers.add_parser(
3049
+ "tasks",
3050
+ help="Show task routing report",
3051
+ )
3052
+ parser_tasks.add_argument(
3053
+ "--hours",
3054
+ type=int,
3055
+ default=24,
3056
+ help="Hours to analyze (default: 24)",
3057
+ )
3058
+ parser_tasks.set_defaults(func=lambda args: _cmd_task_routing_report(args))
3059
+
3060
+ # tests command - test execution status
3061
+ parser_tests = subparsers.add_parser(
3062
+ "tests",
3063
+ help="Show test execution status",
3064
+ )
3065
+ parser_tests.add_argument(
3066
+ "--hours",
3067
+ type=int,
3068
+ default=24,
3069
+ help="Hours to analyze (default: 24)",
3070
+ )
3071
+ parser_tests.set_defaults(func=lambda args: _cmd_test_status(args))
3072
+
3073
+ # agents command - agent performance
3074
+ parser_agents = subparsers.add_parser(
3075
+ "agents",
3076
+ help="Show agent performance metrics",
3077
+ )
3078
+ parser_agents.add_argument(
3079
+ "--hours",
3080
+ type=int,
3081
+ default=168,
3082
+ help="Hours to analyze (default: 168 / 7 days)",
3083
+ )
3084
+ parser_agents.set_defaults(func=lambda args: _cmd_agent_performance(args))
3085
+
2932
3086
  # New command (project scaffolding)
2933
3087
  parser_new = subparsers.add_parser("new", help="Create a new project from a template")
2934
3088
  parser_new.add_argument(
@@ -3018,6 +3172,11 @@ def main():
3018
3172
  help="Force overwrite existing config file",
3019
3173
  )
3020
3174
  parser_workflow.add_argument("--json", action="store_true", help="Output as JSON")
3175
+ parser_workflow.add_argument(
3176
+ "--use-recommended-tier",
3177
+ action="store_true",
3178
+ help="Enable intelligent tier fallback: start with CHEAP tier and automatically upgrade if quality gates fail",
3179
+ )
3021
3180
  parser_workflow.add_argument(
3022
3181
  "--write-tests",
3023
3182
  action="store_true",
@@ -3028,6 +3187,12 @@ def main():
3028
3187
  default="tests/generated",
3029
3188
  help="(test-gen workflow) Output directory for generated tests",
3030
3189
  )
3190
+ parser_workflow.add_argument(
3191
+ "--health-score-threshold",
3192
+ type=int,
3193
+ default=95,
3194
+ help="(health-check workflow) Minimum health score required (0-100, default: 95 for very strict quality)",
3195
+ )
3031
3196
  parser_workflow.set_defaults(func=cmd_workflow)
3032
3197
 
3033
3198
  # Sync-claude command (sync patterns to Claude Code)
empathy_os/cli_unified.py CHANGED
@@ -462,11 +462,36 @@ def workflow_list():
462
462
  def workflow_run(
463
463
  name: str = typer.Argument(..., help="Workflow name"),
464
464
  path: Path = typer.Option(Path(), "--path", "-p", help="Path to run on"),
465
+ use_recommended_tier: bool = typer.Option(
466
+ False,
467
+ "--use-recommended-tier",
468
+ help="Enable intelligent tier fallback: start with CHEAP tier and automatically upgrade if quality gates fail",
469
+ ),
470
+ health_score_threshold: int = typer.Option(
471
+ 95,
472
+ "--health-score-threshold",
473
+ help="(health-check workflow) Minimum health score required (0-100, default: 95 for very strict quality)",
474
+ ),
465
475
  ):
466
476
  """Run a multi-model workflow."""
467
- subprocess.run(
468
- [sys.executable, "-m", "empathy_os.cli", "workflow", "run", name, str(path)], check=False
469
- )
477
+ cmd = [
478
+ sys.executable,
479
+ "-m",
480
+ "empathy_os.cli",
481
+ "workflow",
482
+ "run",
483
+ name,
484
+ "--input",
485
+ f'{{"path": "{path}"}}',
486
+ ]
487
+
488
+ if use_recommended_tier:
489
+ cmd.append("--use-recommended-tier")
490
+
491
+ if health_score_threshold != 95:
492
+ cmd.extend(["--health-score-threshold", str(health_score_threshold)])
493
+
494
+ subprocess.run(cmd, check=False)
470
495
 
471
496
 
472
497
  @workflow_app.command("create")