attune-ai 2.1.5__py3-none-any.whl → 2.2.1__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.
Files changed (125) hide show
  1. attune/cli/__init__.py +3 -59
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +8 -16
  4. attune/cli/commands/provider.py +17 -0
  5. attune/cli/commands/routing.py +3 -1
  6. attune/cli/commands/setup.py +122 -0
  7. attune/cli/commands/tier.py +1 -3
  8. attune/cli/commands/workflow.py +31 -0
  9. attune/cli/parsers/cache.py +1 -0
  10. attune/cli/parsers/help.py +1 -3
  11. attune/cli/parsers/provider.py +7 -0
  12. attune/cli/parsers/routing.py +1 -3
  13. attune/cli/parsers/setup.py +7 -0
  14. attune/cli/parsers/status.py +1 -3
  15. attune/cli/parsers/tier.py +1 -3
  16. attune/cli_minimal.py +9 -3
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/dashboard/app.py +3 -1
  20. attune/dashboard/simple_server.py +3 -1
  21. attune/dashboard/standalone_server.py +7 -3
  22. attune/mcp/server.py +54 -102
  23. attune/memory/long_term.py +0 -2
  24. attune/memory/short_term/__init__.py +84 -0
  25. attune/memory/short_term/base.py +465 -0
  26. attune/memory/short_term/batch.py +219 -0
  27. attune/memory/short_term/caching.py +227 -0
  28. attune/memory/short_term/conflicts.py +265 -0
  29. attune/memory/short_term/cross_session.py +122 -0
  30. attune/memory/short_term/facade.py +653 -0
  31. attune/memory/short_term/pagination.py +207 -0
  32. attune/memory/short_term/patterns.py +271 -0
  33. attune/memory/short_term/pubsub.py +286 -0
  34. attune/memory/short_term/queues.py +244 -0
  35. attune/memory/short_term/security.py +300 -0
  36. attune/memory/short_term/sessions.py +250 -0
  37. attune/memory/short_term/streams.py +242 -0
  38. attune/memory/short_term/timelines.py +234 -0
  39. attune/memory/short_term/transactions.py +184 -0
  40. attune/memory/short_term/working.py +252 -0
  41. attune/meta_workflows/cli_commands/__init__.py +3 -0
  42. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  43. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  44. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  45. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  48. attune/meta_workflows/plan_generator.py +2 -4
  49. attune/models/adaptive_routing.py +4 -8
  50. attune/models/auth_cli.py +3 -9
  51. attune/models/auth_strategy.py +2 -4
  52. attune/models/telemetry/analytics.py +0 -2
  53. attune/models/telemetry/backend.py +0 -3
  54. attune/models/telemetry/storage.py +0 -2
  55. attune/monitoring/alerts.py +6 -10
  56. attune/orchestration/_strategies/__init__.py +156 -0
  57. attune/orchestration/_strategies/base.py +227 -0
  58. attune/orchestration/_strategies/conditional_strategies.py +365 -0
  59. attune/orchestration/_strategies/conditions.py +369 -0
  60. attune/orchestration/_strategies/core_strategies.py +479 -0
  61. attune/orchestration/_strategies/data_classes.py +64 -0
  62. attune/orchestration/_strategies/nesting.py +233 -0
  63. attune/orchestration/execution_strategies.py +58 -1567
  64. attune/orchestration/meta_orchestrator.py +1 -3
  65. attune/project_index/scanner.py +1 -3
  66. attune/project_index/scanner_parallel.py +7 -5
  67. attune/socratic/storage.py +2 -4
  68. attune/socratic_router.py +1 -3
  69. attune/telemetry/agent_coordination.py +9 -3
  70. attune/telemetry/agent_tracking.py +16 -3
  71. attune/telemetry/approval_gates.py +22 -5
  72. attune/telemetry/cli.py +1 -3
  73. attune/telemetry/commands/dashboard_commands.py +24 -8
  74. attune/telemetry/event_streaming.py +8 -2
  75. attune/telemetry/feedback_loop.py +10 -2
  76. attune/tools.py +2 -1
  77. attune/workflow_commands.py +1 -3
  78. attune/workflow_patterns/structural.py +4 -8
  79. attune/workflows/__init__.py +54 -10
  80. attune/workflows/autonomous_test_gen.py +158 -102
  81. attune/workflows/base.py +48 -672
  82. attune/workflows/batch_processing.py +1 -3
  83. attune/workflows/compat.py +156 -0
  84. attune/workflows/cost_mixin.py +141 -0
  85. attune/workflows/data_classes.py +92 -0
  86. attune/workflows/document_gen/workflow.py +11 -14
  87. attune/workflows/history.py +16 -9
  88. attune/workflows/llm_base.py +1 -3
  89. attune/workflows/migration.py +432 -0
  90. attune/workflows/output.py +2 -7
  91. attune/workflows/parsing_mixin.py +427 -0
  92. attune/workflows/perf_audit.py +3 -1
  93. attune/workflows/progress.py +9 -11
  94. attune/workflows/release_prep.py +5 -1
  95. attune/workflows/routing.py +0 -2
  96. attune/workflows/secure_release.py +4 -1
  97. attune/workflows/security_audit.py +20 -14
  98. attune/workflows/security_audit_phase3.py +28 -22
  99. attune/workflows/seo_optimization.py +27 -27
  100. attune/workflows/test_gen/test_templates.py +1 -4
  101. attune/workflows/test_gen/workflow.py +0 -2
  102. attune/workflows/test_gen_behavioral.py +6 -19
  103. attune/workflows/test_gen_parallel.py +8 -6
  104. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/METADATA +4 -3
  105. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/RECORD +121 -96
  106. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/entry_points.txt +0 -2
  107. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  108. attune_llm/agent_factory/__init__.py +6 -6
  109. attune_llm/agent_factory/adapters/haystack_adapter.py +1 -4
  110. attune_llm/commands/__init__.py +10 -10
  111. attune_llm/commands/models.py +3 -3
  112. attune_llm/config/__init__.py +8 -8
  113. attune_llm/learning/__init__.py +3 -3
  114. attune_llm/learning/extractor.py +5 -3
  115. attune_llm/learning/storage.py +5 -3
  116. attune_llm/security/__init__.py +17 -17
  117. attune_llm/utils/tokens.py +3 -1
  118. attune/cli_legacy.py +0 -3978
  119. attune/memory/short_term.py +0 -2192
  120. attune/workflows/manage_docs.py +0 -87
  121. attune/workflows/test5.py +0 -125
  122. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/WHEEL +0 -0
  123. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE +0 -0
  124. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  125. {attune_ai-2.1.5.dist-info → attune_ai-2.2.1.dist-info}/top_level.txt +0 -0
attune/cli/__init__.py CHANGED
@@ -60,21 +60,9 @@ def main() -> int:
60
60
 
61
61
  register_all_parsers(subparsers)
62
62
 
63
- # TODO: Import and register remaining commands from cli.py
64
- # This is a partial refactoring - additional commands still in cli.py
65
- # For now, if command not found in new structure, fall back to old cli.py
66
- #
67
- # NOTE: Temporarily disabled to avoid conflicts with extracted commands.
68
- # Commands that have been extracted:
69
- # - help, tier, info, patterns, status (Phase 1)
70
- # - workflow, inspect (run, inspect, export, import) (Phase 2)
71
- # Once all commands are extracted, the old cli.py will be removed entirely.
72
- #
73
- # try:
74
- # from attune import cli as old_cli
75
- # _register_legacy_commands(subparsers, old_cli)
76
- # except ImportError:
77
- # pass # Old cli.py not available or already moved
63
+ # NOTE: CLI refactoring is COMPLETE (v2.1.5)
64
+ # All 30 commands have been extracted from the monolithic cli_legacy.py
65
+ # to the new modular structure in cli/commands/ and cli/parsers/.
78
66
 
79
67
  # Parse arguments
80
68
  args = parser.parse_args()
@@ -107,50 +95,6 @@ def main() -> int:
107
95
  return 0
108
96
 
109
97
 
110
- def _register_legacy_commands(subparsers, old_cli):
111
- """Temporarily register commands not yet extracted from old cli.py.
112
-
113
- This function provides backward compatibility during the refactoring process.
114
- As commands are extracted into the new structure, they should be removed
115
- from this registration.
116
-
117
- Args:
118
- subparsers: ArgumentParser subparsers object
119
- old_cli: Reference to old cli module
120
-
121
- Note:
122
- This is a TEMPORARY function that will be removed once all commands
123
- are extracted from the monolithic cli.py file.
124
- """
125
- # Import command functions that haven't been extracted yet
126
- try:
127
- # Patterns commands
128
- from attune.cli.commands.patterns import (
129
- cmd_patterns_export,
130
- cmd_patterns_list,
131
- cmd_patterns_resolve,
132
- )
133
-
134
- patterns_parser = subparsers.add_parser("patterns", help="Pattern management")
135
- patterns_sub = patterns_parser.add_subparsers(dest="patterns_command")
136
-
137
- p_list = patterns_sub.add_parser("list", help="List patterns")
138
- p_list.set_defaults(func=cmd_patterns_list)
139
-
140
- p_export = patterns_sub.add_parser("export", help="Export patterns")
141
- p_export.add_argument("output", help="Output file")
142
- p_export.set_defaults(func=cmd_patterns_export)
143
-
144
- p_resolve = patterns_sub.add_parser("resolve", help="Resolve pattern")
145
- p_resolve.add_argument("pattern_id", help="Pattern ID")
146
- p_resolve.set_defaults(func=cmd_patterns_resolve)
147
- except (ImportError, AttributeError):
148
- pass # Commands not available
149
-
150
- # Add other legacy commands as needed...
151
- # This list will shrink as commands are extracted
152
-
153
-
154
98
  # Preserve backward compatibility
155
99
  if __name__ == "__main__":
156
100
  sys.exit(main())
@@ -184,12 +184,8 @@ def cmd_batch_results(args):
184
184
  print(f" Total: {len(results)} results")
185
185
 
186
186
  # Summary
187
- succeeded = sum(
188
- 1 for r in results if r.get("result", {}).get("type") == "succeeded"
189
- )
190
- errored = sum(
191
- 1 for r in results if r.get("result", {}).get("type") == "errored"
192
- )
187
+ succeeded = sum(1 for r in results if r.get("result", {}).get("type") == "succeeded")
188
+ errored = sum(1 for r in results if r.get("result", {}).get("type") == "errored")
193
189
 
194
190
  print(f" Succeeded: {succeeded}")
195
191
  print(f" Errored: {errored}")
@@ -242,12 +238,8 @@ def cmd_batch_wait(args):
242
238
  print(f" Total: {len(results)} results")
243
239
 
244
240
  # Summary
245
- succeeded = sum(
246
- 1 for r in results if r.get("result", {}).get("type") == "succeeded"
247
- )
248
- errored = sum(
249
- 1 for r in results if r.get("result", {}).get("type") == "errored"
250
- )
241
+ succeeded = sum(1 for r in results if r.get("result", {}).get("type") == "succeeded")
242
+ errored = sum(1 for r in results if r.get("result", {}).get("type") == "errored")
251
243
 
252
244
  print(f" Succeeded: {succeeded}")
253
245
  print(f" Errored: {errored}")
@@ -55,7 +55,7 @@ def _collect_cache_stats(days: int = 7) -> dict[str, Any]:
55
55
  log_paths = [
56
56
  Path.cwd() / "attune.log",
57
57
  Path.home() / ".empathy" / "logs" / "attune.log",
58
- Path("/tmp/attune.log"),
58
+ Path("/tmp/attune.log"), # nosec B108 - temp log file location
59
59
  ]
60
60
 
61
61
  log_file = None
@@ -93,14 +93,10 @@ def _collect_cache_stats(days: int = 7) -> dict[str, Any]:
93
93
  for line in f:
94
94
  # Try to extract timestamp
95
95
  # Common format: 2026-01-27 21:30:45,123
96
- timestamp_match = re.match(
97
- r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", line
98
- )
96
+ timestamp_match = re.match(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", line)
99
97
  if timestamp_match:
100
98
  try:
101
- log_time = datetime.strptime(
102
- timestamp_match.group(1), "%Y-%m-%d %H:%M:%S"
103
- )
99
+ log_time = datetime.strptime(timestamp_match.group(1), "%Y-%m-%d %H:%M:%S")
104
100
  if log_time < cutoff_date:
105
101
  continue # Skip old entries
106
102
  except ValueError:
@@ -140,9 +136,7 @@ def _collect_cache_stats(days: int = 7) -> dict[str, Any]:
140
136
  }
141
137
 
142
138
  # Calculate metrics
143
- cache_hit_rate = (
144
- (cache_hits / total_requests * 100) if total_requests > 0 else 0.0
145
- )
139
+ cache_hit_rate = (cache_hits / total_requests * 100) if total_requests > 0 else 0.0
146
140
 
147
141
  return {
148
142
  "days_analyzed": days,
@@ -154,9 +148,7 @@ def _collect_cache_stats(days: int = 7) -> dict[str, Any]:
154
148
  "total_cache_read_tokens": total_cache_read_tokens,
155
149
  "total_cache_write_tokens": total_cache_write_tokens,
156
150
  "total_savings": round(total_savings, 4),
157
- "avg_savings_per_hit": (
158
- round(total_savings / cache_hits, 4) if cache_hits > 0 else 0.0
159
- ),
151
+ "avg_savings_per_hit": (round(total_savings / cache_hits, 4) if cache_hits > 0 else 0.0),
160
152
  }
161
153
 
162
154
 
@@ -193,7 +185,7 @@ def _display_cache_report(stats: dict[str, Any], verbose: bool = False):
193
185
  # Cost savings
194
186
  print("💰 Cost Savings:")
195
187
  print(f" Total Saved: ${stats['total_savings']:.4f}")
196
- if stats['cache_hits'] > 0:
188
+ if stats["cache_hits"] > 0:
197
189
  print(f" Avg Savings per Hit: ${stats['avg_savings_per_hit']:.4f}")
198
190
  print()
199
191
 
@@ -205,7 +197,7 @@ def _display_cache_report(stats: dict[str, Any], verbose: bool = False):
205
197
  print()
206
198
 
207
199
  # Performance assessment
208
- hit_rate = stats['cache_hit_rate']
200
+ hit_rate = stats["cache_hit_rate"]
209
201
  print("📈 Performance Assessment:")
210
202
  if hit_rate >= 50:
211
203
  print(" ✅ EXCELLENT - Cache is working effectively")
@@ -222,7 +214,7 @@ def _display_cache_report(stats: dict[str, Any], verbose: bool = False):
222
214
  print()
223
215
 
224
216
  # Recommendations
225
- if stats['total_requests'] < 10:
217
+ if stats["total_requests"] < 10:
226
218
  print("ℹ️ Note: Limited data available. Run more workflows for accurate stats.")
227
219
  elif hit_rate < 30:
228
220
  print("💡 Recommendations:")
@@ -96,3 +96,20 @@ def cmd_provider_set(args):
96
96
 
97
97
  print(f"✓ Default provider set to: {provider}")
98
98
  print(f" Saved to: {validated_workflows_path}")
99
+
100
+
101
+ def cmd_provider_hybrid(args):
102
+ """Configure hybrid mode (DEPRECATED in v5.0.0).
103
+
104
+ Hybrid mode is no longer supported. This command now configures
105
+ Anthropic as the sole provider.
106
+
107
+ Args:
108
+ args: Namespace object from argparse (no additional attributes used).
109
+
110
+ Returns:
111
+ None: Launches interactive Anthropic configuration.
112
+ """
113
+ from attune.models.provider_config import configure_hybrid_interactive
114
+
115
+ configure_hybrid_interactive()
@@ -82,7 +82,9 @@ def cmd_routing_stats(args: Any) -> int:
82
82
  )
83
83
 
84
84
  print(f" Best model: {best_model[0]}")
85
- print(f" ({best_model[1]['success_rate']:.1%} success, ${best_model[1]['avg_cost']:.4f}/call)")
85
+ print(
86
+ f" ({best_model[1]['success_rate']:.1%} success, ${best_model[1]['avg_cost']:.4f}/call)"
87
+ )
86
88
 
87
89
  # Cost savings potential
88
90
  if len(stats["models_used"]) > 1:
@@ -94,3 +94,125 @@ def cmd_validate(args):
94
94
  logger.exception(f"Unexpected error validating configuration: {e}")
95
95
  logger.error(f"✗ Configuration invalid: {e}")
96
96
  sys.exit(1)
97
+
98
+
99
+ def cmd_setup(args):
100
+ """Interactive setup workflow.
101
+
102
+ Guides user through initial framework configuration step by step.
103
+ Updated for Claude-native v5.0.0 (Anthropic-only).
104
+
105
+ Args:
106
+ args: Namespace object from argparse (no additional attributes used).
107
+
108
+ Returns:
109
+ None: Creates attune.config.yml with user's choices.
110
+ """
111
+ print("🧙 Attune AI Setup Workflow")
112
+ print("=" * 50)
113
+ print("\nI'll help you set up your Attune AI configuration.\n")
114
+
115
+ # Step 1: Use case
116
+ print("1. What's your primary use case?")
117
+ print(" [1] Software development")
118
+ print(" [2] Healthcare applications")
119
+ print(" [3] Customer support")
120
+ print(" [4] Other")
121
+
122
+ use_case_choice = input("\nYour choice (1-4): ").strip()
123
+ use_case_map = {
124
+ "1": "software_development",
125
+ "2": "healthcare",
126
+ "3": "customer_support",
127
+ "4": "general",
128
+ }
129
+ use_case = use_case_map.get(use_case_choice, "general")
130
+
131
+ # Step 2: Empathy level
132
+ print("\n2. What empathy level do you want to target?")
133
+ print(" [1] Level 1 - Reactive (basic Q&A)")
134
+ print(" [2] Level 2 - Guided (asks clarifying questions)")
135
+ print(" [3] Level 3 - Proactive (offers improvements)")
136
+ print(" [4] Level 4 - Anticipatory (predicts problems) ⭐ Recommended")
137
+ print(" [5] Level 5 - Transformative (reshapes workflows)")
138
+
139
+ level_choice = input("\nYour choice (1-5) [4]: ").strip() or "4"
140
+ target_level = int(level_choice) if level_choice in ["1", "2", "3", "4", "5"] else 4
141
+
142
+ # Step 3: LLM provider (Anthropic-only as of v5.0.0)
143
+ print("\n3. LLM Provider: Anthropic Claude")
144
+ print(" Attune AI is Claude-native and uses Anthropic exclusively.")
145
+
146
+ from attune.models.provider_config import ProviderConfig
147
+
148
+ config_detected = ProviderConfig.auto_detect()
149
+ if config_detected.available_providers:
150
+ print(" ✓ ANTHROPIC_API_KEY detected")
151
+ else:
152
+ print(" ⚠️ ANTHROPIC_API_KEY not detected")
153
+ print(" Set your API key: export ANTHROPIC_API_KEY='your-key-here'")
154
+ print(" Get key at: https://console.anthropic.com/settings/keys")
155
+
156
+ # Step 4: User ID
157
+ print("\n4. What user ID should we use?")
158
+ user_id = input("User ID [default_user]: ").strip() or "default_user"
159
+
160
+ # Generate configuration
161
+ config = {
162
+ "user_id": user_id,
163
+ "target_level": target_level,
164
+ "confidence_threshold": 0.75,
165
+ "persistence_enabled": True,
166
+ "persistence_backend": "sqlite",
167
+ "persistence_path": ".attune",
168
+ "metrics_enabled": True,
169
+ "use_case": use_case,
170
+ "llm_provider": "anthropic",
171
+ }
172
+
173
+ # Save configuration
174
+ output_file = "attune.config.yml"
175
+ print(f"\n5. Creating configuration file: {output_file}")
176
+
177
+ # Write YAML config
178
+ yaml_content = f"""# Attune AI Configuration
179
+ # Generated by setup workflow
180
+
181
+ # Core settings
182
+ user_id: "{config["user_id"]}"
183
+ target_level: {config["target_level"]}
184
+ confidence_threshold: {config["confidence_threshold"]}
185
+
186
+ # Use case
187
+ use_case: "{config["use_case"]}"
188
+
189
+ # Persistence
190
+ persistence_enabled: {str(config["persistence_enabled"]).lower()}
191
+ persistence_backend: "{config["persistence_backend"]}"
192
+ persistence_path: "{config["persistence_path"]}"
193
+
194
+ # Metrics
195
+ metrics_enabled: {str(config["metrics_enabled"]).lower()}
196
+
197
+ # LLM Provider (Claude-native v5.0.0)
198
+ llm_provider: "anthropic"
199
+ """
200
+
201
+ validated_output = _validate_file_path(output_file)
202
+ with open(validated_output, "w") as f:
203
+ f.write(yaml_content)
204
+
205
+ print(f" ✓ Created {validated_output}")
206
+
207
+ print("\n" + "=" * 50)
208
+ print("✅ Setup complete!")
209
+ print("\nNext steps:")
210
+ print(f" 1. Edit {output_file} to customize settings")
211
+
212
+ if not config_detected.available_providers:
213
+ print(" 2. Set ANTHROPIC_API_KEY environment variable")
214
+ print(" 3. Run: attune run --config attune.config.yml")
215
+ else:
216
+ print(" 2. Run: attune run --config attune.config.yml")
217
+
218
+ print("\nHappy coding! 🧠✨\n")
@@ -109,9 +109,7 @@ def cmd_tier_stats(args):
109
109
 
110
110
  print(" BUG TYPE DISTRIBUTION")
111
111
  print(" " + "-" * 40)
112
- sorted_types = sorted(
113
- stats["bug_type_distribution"].items(), key=lambda x: x[1], reverse=True
114
- )
112
+ sorted_types = sorted(stats["bug_type_distribution"].items(), key=lambda x: x[1], reverse=True)
115
113
  for bug_type, count in sorted_types[:10]:
116
114
  percent = (count / stats["total_patterns"]) * 100
117
115
  print(f" {bug_type:20} {count:3} ({percent:5.1f}%)")
@@ -14,6 +14,11 @@ from attune.logging_config import get_logger
14
14
  from attune.workflows import get_workflow
15
15
  from attune.workflows import list_workflows as get_workflow_list
16
16
  from attune.workflows.config import WorkflowConfig, create_example_config
17
+ from attune.workflows.migration import (
18
+ WORKFLOW_ALIASES,
19
+ resolve_workflow_migration,
20
+ show_migration_tip,
21
+ )
17
22
 
18
23
  logger = get_logger(__name__)
19
24
 
@@ -127,6 +132,13 @@ def cmd_workflow(args):
127
132
  return 1
128
133
 
129
134
  try:
135
+ # Check for workflow migration (describe still works with old names)
136
+ if name in WORKFLOW_ALIASES:
137
+ resolved_name, migration_kwargs, _ = resolve_workflow_migration(name)
138
+ if resolved_name and resolved_name != name:
139
+ print(f"ℹ️ '{name}' is now '{resolved_name}'")
140
+ name = resolved_name
141
+
130
142
  workflow_cls = get_workflow(name)
131
143
  provider = getattr(args, "provider", None)
132
144
  workflow = workflow_cls(provider=provider)
@@ -165,6 +177,16 @@ def cmd_workflow(args):
165
177
  return 1
166
178
 
167
179
  try:
180
+ # Check for workflow migration (environment-aware)
181
+ original_name = name
182
+ migration_kwargs = {}
183
+ was_migrated = False
184
+
185
+ if name in WORKFLOW_ALIASES:
186
+ name, migration_kwargs, was_migrated = resolve_workflow_migration(name)
187
+ if was_migrated:
188
+ logger.info(f"Migrated '{original_name}' -> '{name}' with {migration_kwargs}")
189
+
168
190
  workflow_cls = get_workflow(name)
169
191
 
170
192
  # Get provider from CLI arg, or fall back to config's default_provider
@@ -197,6 +219,11 @@ def cmd_workflow(args):
197
219
  health_score_threshold = getattr(args, "health_score_threshold", 100)
198
220
  workflow_kwargs["health_score_threshold"] = health_score_threshold
199
221
 
222
+ # Merge migration kwargs (from workflow consolidation)
223
+ for key, value in migration_kwargs.items():
224
+ if not key.startswith("_") and key in init_params:
225
+ workflow_kwargs[key] = value
226
+
200
227
  workflow = workflow_cls(**workflow_kwargs)
201
228
 
202
229
  # Parse input
@@ -367,6 +394,10 @@ def cmd_workflow(args):
367
394
  error_msg = error_msg or "Unknown error"
368
395
  print(f"\n✗ Workflow failed: {error_msg}\n")
369
396
 
397
+ # Show migration tip if workflow was migrated (helps users learn new syntax)
398
+ if was_migrated and original_name != name:
399
+ show_migration_tip(original_name, name, migration_kwargs)
400
+
370
401
  except KeyError:
371
402
  print(f"Error: Workflow '{name}' not found")
372
403
  print("\nRun 'empathy workflow list' to see available workflows")
@@ -15,6 +15,7 @@ def register_parsers(subparsers):
15
15
  None: Adds cache subparser with stats and clear subcommands
16
16
  """
17
17
  from ..commands.cache import cmd_cache_clear, cmd_cache_stats
18
+
18
19
  # Main cache command
19
20
  cache_parser = subparsers.add_parser(
20
21
  "cache",
@@ -20,9 +20,7 @@ def register_parsers(subparsers):
20
20
  # cheatsheet command
21
21
  parser_cheatsheet = subparsers.add_parser("cheatsheet", help="Quick reference guide")
22
22
  parser_cheatsheet.add_argument("--category", help="Specific category to show")
23
- parser_cheatsheet.add_argument(
24
- "--compact", action="store_true", help="Show commands only"
25
- )
23
+ parser_cheatsheet.add_argument("--compact", action="store_true", help="Show commands only")
26
24
  parser_cheatsheet.set_defaults(func=help_commands.cmd_cheatsheet)
27
25
 
28
26
  # onboard command
@@ -38,3 +38,10 @@ def register_parsers(subparsers):
38
38
  help="Provider name (anthropic only)",
39
39
  )
40
40
  p_set.set_defaults(func=provider.cmd_provider_set)
41
+
42
+ # Provider hybrid command (deprecated)
43
+ p_hybrid = provider_sub.add_parser(
44
+ "hybrid",
45
+ help="Configure hybrid mode (DEPRECATED - now Anthropic only)",
46
+ )
47
+ p_hybrid.set_defaults(func=provider.cmd_provider_hybrid)
@@ -28,9 +28,7 @@ def register_parsers(subparsers):
28
28
  )
29
29
 
30
30
  # Routing subcommands
31
- routing_subparsers = routing_parser.add_subparsers(
32
- dest="routing_command", required=True
33
- )
31
+ routing_subparsers = routing_parser.add_subparsers(dest="routing_command", required=True)
34
32
 
35
33
  # routing stats command
36
34
  stats_parser = routing_subparsers.add_parser(
@@ -40,3 +40,10 @@ def register_parsers(subparsers):
40
40
  help="Path to configuration file to validate",
41
41
  )
42
42
  parser_validate.set_defaults(func=setup.cmd_validate)
43
+
44
+ # Setup wizard command
45
+ parser_setup = subparsers.add_parser(
46
+ "setup",
47
+ help="Interactive setup workflow for first-time configuration",
48
+ )
49
+ parser_setup.set_defaults(func=setup.cmd_setup)
@@ -36,9 +36,7 @@ def register_parsers(subparsers):
36
36
  parser_health.add_argument("--deep", action="store_true", help="Run all checks")
37
37
  parser_health.add_argument("--fix", action="store_true", help="Auto-fix issues")
38
38
  parser_health.add_argument("--dry-run", action="store_true", help="Preview fixes only")
39
- parser_health.add_argument(
40
- "--interactive", action="store_true", help="Interactive fix mode"
41
- )
39
+ parser_health.add_argument("--interactive", action="store_true", help="Interactive fix mode")
42
40
  parser_health.add_argument("--project-root", default=".", help="Project root")
43
41
  parser_health.add_argument("--details", action="store_true", help="Show details")
44
42
  parser_health.add_argument("--full", action="store_true", help="Show all details")
@@ -18,9 +18,7 @@ def register_parsers(subparsers):
18
18
  tier_subparsers = tier_parser.add_subparsers(dest="tier_command")
19
19
 
20
20
  # tier recommend
21
- parser_recommend = tier_subparsers.add_parser(
22
- "recommend", help="Get tier recommendation"
23
- )
21
+ parser_recommend = tier_subparsers.add_parser("recommend", help="Get tier recommendation")
24
22
  parser_recommend.add_argument("description", help="Bug or task description")
25
23
  parser_recommend.add_argument("--files", help="Comma-separated list of files")
26
24
  parser_recommend.add_argument(
attune/cli_minimal.py CHANGED
@@ -1073,8 +1073,12 @@ Documentation: https://smartaimemory.com/framework-docs/
1073
1073
 
1074
1074
  # dashboard start
1075
1075
  start_parser = dashboard_sub.add_parser("start", help="Start dashboard web server")
1076
- start_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
1077
- start_parser.add_argument("--port", type=int, default=8000, help="Port to bind to (default: 8000)")
1076
+ start_parser.add_argument(
1077
+ "--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)"
1078
+ )
1079
+ start_parser.add_argument(
1080
+ "--port", type=int, default=8000, help="Port to bind to (default: 8000)"
1081
+ )
1078
1082
 
1079
1083
  # --- Utility commands ---
1080
1084
  subparsers.add_parser("validate", help="Validate configuration")
@@ -1125,7 +1129,9 @@ def main(argv: list[str] | None = None) -> int:
1125
1129
  elif args.telemetry_command == "signals":
1126
1130
  return cmd_telemetry_signals(args)
1127
1131
  else:
1128
- print("Usage: attune telemetry {show|savings|export|routing-stats|routing-check|models|agents|signals}")
1132
+ print(
1133
+ "Usage: attune telemetry {show|savings|export|routing-stats|routing-check|models|agents|signals}"
1134
+ )
1129
1135
  return 1
1130
1136
 
1131
1137
  elif args.command == "provider":
attune/cli_router.py CHANGED
@@ -195,9 +195,7 @@ class HybridRouter:
195
195
  with open(self.preferences_path, "w") as f:
196
196
  yaml.dump(data, f, default_flow_style=False)
197
197
 
198
- async def route(
199
- self, user_input: str, context: dict[str, Any] | None = None
200
- ) -> dict[str, Any]:
198
+ async def route(self, user_input: str, context: dict[str, Any] | None = None) -> dict[str, Any]:
201
199
  """Route user input to appropriate command or workflow.
202
200
 
203
201
  Args:
@@ -242,7 +240,8 @@ class HybridRouter:
242
240
  "args": args,
243
241
  "original": command,
244
242
  "confidence": 1.0,
245
- "instruction": f"Use Skill tool with skill='{skill}'" + (f", args='{args}'" if args else ""),
243
+ "instruction": f"Use Skill tool with skill='{skill}'"
244
+ + (f", args='{args}'" if args else ""),
246
245
  }
247
246
 
248
247
  def _infer_command(self, keyword: str) -> dict[str, Any] | None:
@@ -271,7 +270,8 @@ class HybridRouter:
271
270
  "original": keyword,
272
271
  "confidence": pref.confidence,
273
272
  "source": "learned",
274
- "instruction": f"Use Skill tool with skill='{pref.skill}'" + (f", args='{pref.args}'" if pref.args else ""),
273
+ "instruction": f"Use Skill tool with skill='{pref.skill}'"
274
+ + (f", args='{pref.args}'" if pref.args else ""),
275
275
  }
276
276
 
277
277
  # Check built-in keyword map
@@ -284,7 +284,8 @@ class HybridRouter:
284
284
  "original": keyword,
285
285
  "confidence": 0.9,
286
286
  "source": "builtin",
287
- "instruction": f"Use Skill tool with skill='{skill}'" + (f", args='{args}'" if args else ""),
287
+ "instruction": f"Use Skill tool with skill='{skill}'"
288
+ + (f", args='{args}'" if args else ""),
288
289
  }
289
290
 
290
291
  # Check for hub names (show hub menu)
@@ -329,7 +330,8 @@ class HybridRouter:
329
330
  "reasoning": decision.reasoning,
330
331
  "original": text,
331
332
  "source": "natural_language",
332
- "instruction": f"Use Skill tool with skill='{skill}'" + (f", args='{args}'" if args else ""),
333
+ "instruction": f"Use Skill tool with skill='{skill}'"
334
+ + (f", args='{args}'" if args else ""),
333
335
  }
334
336
 
335
337
  def _workflow_to_skill(self, workflow: str) -> tuple[str, str]:
attune/cli_unified.py CHANGED
@@ -30,6 +30,9 @@ Copyright 2025 Smart-AI-Memory
30
30
  Licensed under Fair Source License 0.9
31
31
  """
32
32
 
33
+ # ruff: noqa: E402
34
+ # Deprecation warning must be shown before other imports
35
+
33
36
  import warnings
34
37
 
35
38
  warnings.warn(
attune/dashboard/app.py CHANGED
@@ -377,7 +377,9 @@ async def get_underperforming_stages(threshold: float = 0.7):
377
377
  all_underperforming = []
378
378
 
379
379
  for workflow in workflows:
380
- underperforming = feedback.get_underperforming_stages(workflow, quality_threshold=threshold)
380
+ underperforming = feedback.get_underperforming_stages(
381
+ workflow, quality_threshold=threshold
382
+ )
381
383
  for stage_name, stats in underperforming:
382
384
  all_underperforming.append(
383
385
  {
@@ -378,7 +378,9 @@ class DashboardHandler(BaseHTTPRequestHandler):
378
378
  all_underperforming = []
379
379
 
380
380
  for workflow in workflows:
381
- underperforming = feedback.get_underperforming_stages(workflow, quality_threshold=threshold)
381
+ underperforming = feedback.get_underperforming_stages(
382
+ workflow, quality_threshold=threshold
383
+ )
382
384
  for stage_name, stats in underperforming:
383
385
  all_underperforming.append(
384
386
  {
@@ -305,7 +305,11 @@ class StandaloneDashboardHandler(BaseHTTPRequestHandler):
305
305
 
306
306
  result.append(
307
307
  {
308
- "event_id": entry_id.decode("utf-8") if isinstance(entry_id, bytes) else entry_id,
308
+ "event_id": (
309
+ entry_id.decode("utf-8")
310
+ if isinstance(entry_id, bytes)
311
+ else entry_id
312
+ ),
309
313
  "event_type": event_type,
310
314
  "timestamp": timestamp,
311
315
  "data": data,
@@ -423,7 +427,7 @@ class StandaloneDashboardHandler(BaseHTTPRequestHandler):
423
427
 
424
428
  # Calculate stats
425
429
  result = []
426
- for group_key, group in feedback_groups.items():
430
+ for _group_key, group in feedback_groups.items():
427
431
  qualities = group["qualities"]
428
432
  if qualities:
429
433
  avg_quality = sum(qualities) / len(qualities)
@@ -476,7 +480,7 @@ class StandaloneDashboardHandler(BaseHTTPRequestHandler):
476
480
 
477
481
  # Find underperforming stages
478
482
  result = []
479
- for group_key, group in feedback_groups.items():
483
+ for _group_key, group in feedback_groups.items():
480
484
  qualities = group["qualities"]
481
485
  if qualities:
482
486
  avg_quality = sum(qualities) / len(qualities)