attune-ai 2.1.4__py3-none-any.whl → 2.2.0__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 (123) hide show
  1. attune/cli/__init__.py +3 -55
  2. attune/cli/commands/batch.py +4 -12
  3. attune/cli/commands/cache.py +7 -15
  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 +34 -28
  17. attune/cli_router.py +9 -7
  18. attune/cli_unified.py +3 -0
  19. attune/core.py +190 -0
  20. attune/dashboard/app.py +4 -2
  21. attune/dashboard/simple_server.py +3 -1
  22. attune/dashboard/standalone_server.py +7 -3
  23. attune/mcp/server.py +54 -102
  24. attune/memory/long_term.py +0 -2
  25. attune/memory/short_term/__init__.py +84 -0
  26. attune/memory/short_term/base.py +467 -0
  27. attune/memory/short_term/batch.py +219 -0
  28. attune/memory/short_term/caching.py +227 -0
  29. attune/memory/short_term/conflicts.py +265 -0
  30. attune/memory/short_term/cross_session.py +122 -0
  31. attune/memory/short_term/facade.py +655 -0
  32. attune/memory/short_term/pagination.py +215 -0
  33. attune/memory/short_term/patterns.py +271 -0
  34. attune/memory/short_term/pubsub.py +286 -0
  35. attune/memory/short_term/queues.py +244 -0
  36. attune/memory/short_term/security.py +300 -0
  37. attune/memory/short_term/sessions.py +250 -0
  38. attune/memory/short_term/streams.py +249 -0
  39. attune/memory/short_term/timelines.py +234 -0
  40. attune/memory/short_term/transactions.py +186 -0
  41. attune/memory/short_term/working.py +252 -0
  42. attune/meta_workflows/cli_commands/__init__.py +3 -0
  43. attune/meta_workflows/cli_commands/agent_commands.py +0 -4
  44. attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
  45. attune/meta_workflows/cli_commands/config_commands.py +0 -5
  46. attune/meta_workflows/cli_commands/memory_commands.py +0 -5
  47. attune/meta_workflows/cli_commands/template_commands.py +0 -5
  48. attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
  49. attune/meta_workflows/workflow.py +1 -1
  50. attune/models/adaptive_routing.py +4 -8
  51. attune/models/auth_cli.py +3 -9
  52. attune/models/auth_strategy.py +2 -4
  53. attune/models/provider_config.py +20 -1
  54. attune/models/telemetry/analytics.py +0 -2
  55. attune/models/telemetry/backend.py +0 -3
  56. attune/models/telemetry/storage.py +0 -2
  57. attune/orchestration/_strategies/__init__.py +156 -0
  58. attune/orchestration/_strategies/base.py +231 -0
  59. attune/orchestration/_strategies/conditional_strategies.py +373 -0
  60. attune/orchestration/_strategies/conditions.py +369 -0
  61. attune/orchestration/_strategies/core_strategies.py +491 -0
  62. attune/orchestration/_strategies/data_classes.py +64 -0
  63. attune/orchestration/_strategies/nesting.py +233 -0
  64. attune/orchestration/execution_strategies.py +58 -1567
  65. attune/orchestration/meta_orchestrator.py +1 -3
  66. attune/project_index/scanner.py +1 -3
  67. attune/project_index/scanner_parallel.py +7 -5
  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 +3 -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 +1 -0
  77. attune/workflow_commands.py +1 -3
  78. attune/workflows/__init__.py +53 -10
  79. attune/workflows/autonomous_test_gen.py +160 -104
  80. attune/workflows/base.py +48 -664
  81. attune/workflows/batch_processing.py +2 -4
  82. attune/workflows/compat.py +156 -0
  83. attune/workflows/cost_mixin.py +141 -0
  84. attune/workflows/data_classes.py +92 -0
  85. attune/workflows/document_gen/workflow.py +11 -14
  86. attune/workflows/history.py +62 -37
  87. attune/workflows/llm_base.py +2 -4
  88. attune/workflows/migration.py +422 -0
  89. attune/workflows/output.py +3 -9
  90. attune/workflows/parsing_mixin.py +427 -0
  91. attune/workflows/perf_audit.py +3 -1
  92. attune/workflows/progress.py +10 -13
  93. attune/workflows/release_prep.py +5 -1
  94. attune/workflows/routing.py +0 -2
  95. attune/workflows/secure_release.py +2 -1
  96. attune/workflows/security_audit.py +19 -14
  97. attune/workflows/security_audit_phase3.py +28 -22
  98. attune/workflows/seo_optimization.py +29 -29
  99. attune/workflows/test_gen/test_templates.py +1 -4
  100. attune/workflows/test_gen/workflow.py +0 -2
  101. attune/workflows/test_gen_behavioral.py +7 -20
  102. attune/workflows/test_gen_parallel.py +6 -4
  103. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
  104. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/RECORD +119 -94
  105. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
  106. attune_healthcare/monitors/monitoring/__init__.py +9 -9
  107. attune_llm/agent_factory/__init__.py +6 -6
  108. attune_llm/commands/__init__.py +10 -10
  109. attune_llm/commands/models.py +3 -3
  110. attune_llm/config/__init__.py +8 -8
  111. attune_llm/learning/__init__.py +3 -3
  112. attune_llm/learning/extractor.py +5 -3
  113. attune_llm/learning/storage.py +5 -3
  114. attune_llm/security/__init__.py +17 -17
  115. attune_llm/utils/tokens.py +3 -1
  116. attune/cli_legacy.py +0 -3957
  117. attune/memory/short_term.py +0 -2192
  118. attune/workflows/manage_docs.py +0 -87
  119. attune/workflows/test5.py +0 -125
  120. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
  121. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
  122. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
  123. {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,422 @@
1
+ """Workflow Migration System
2
+
3
+ Environment-aware workflow migration with config-driven preferences.
4
+ Helps users transition from deprecated workflows to consolidated versions.
5
+
6
+ Behavior:
7
+ - CI/Non-interactive: Silent aliasing with log message
8
+ - First interactive use: Show dialog with "remember" option
9
+ - Subsequent uses: Follow stored preference
10
+ - Config override: Users can set migration mode globally
11
+
12
+ Usage:
13
+ from attune.workflows.migration import resolve_workflow_migration
14
+
15
+ # Returns (canonical_name, kwargs, was_migrated)
16
+ name, kwargs, migrated = resolve_workflow_migration("test-gen-behavioral")
17
+
18
+ Copyright 2026 Smart-AI-Memory
19
+ Licensed under Fair Source License 0.9
20
+ """
21
+
22
+ import json
23
+ import logging
24
+ import os
25
+ import sys
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Migration mode options
33
+ MIGRATION_MODE_PROMPT = "prompt" # Ask user on first use
34
+ MIGRATION_MODE_AUTO = "auto" # Silently use new syntax
35
+ MIGRATION_MODE_LEGACY = "legacy" # Keep using old syntax (deprecated)
36
+
37
+ # Workflow aliases: old_name -> (new_name, default_kwargs)
38
+ WORKFLOW_ALIASES: dict[str, tuple[str, dict[str, Any]]] = {
39
+ # Code Review consolidation
40
+ "pro-review": ("code-review", {"mode": "premium"}),
41
+ # Test Generation consolidation
42
+ "test-gen-behavioral": ("test-gen", {"style": "behavioral"}),
43
+ "test-coverage-boost": ("test-gen", {"target": "coverage"}),
44
+ "autonomous-test-gen": ("test-gen-parallel", {"autonomous": True}),
45
+ "progressive-test-gen": ("test-gen-parallel", {"progressive": True}),
46
+ # Release consolidation
47
+ "secure-release": ("release-prep", {"mode": "secure"}),
48
+ "orchestrated-release-prep": ("release-prep", {"mode": "full"}),
49
+ # Deprecated (show warning, map to replacement)
50
+ "release-prep-legacy": ("release-prep", {"mode": "standard", "_deprecated": True}),
51
+ "document-manager": ("doc-gen", {"_deprecated": True}),
52
+ # Removed (show error with migration path)
53
+ "test5": (None, {"_removed": True, "_message": "test5 was a test artifact and has been removed"}),
54
+ "manage-docs": (
55
+ None,
56
+ {"_removed": True, "_message": "manage-docs was incomplete. Use doc-gen or doc-orchestrator"},
57
+ ),
58
+ }
59
+
60
+ # Human-readable descriptions for the dialog
61
+ WORKFLOW_DESCRIPTIONS: dict[str, str] = {
62
+ "code-review": "Comprehensive code analysis with optional premium mode",
63
+ "test-gen": "Generate tests with style (standard/behavioral) and target (gaps/coverage) options",
64
+ "test-gen-parallel": "Batch test generation with autonomous and progressive modes",
65
+ "release-prep": "Release readiness with modes: standard, secure, or full",
66
+ "doc-gen": "Generate documentation for code",
67
+ }
68
+
69
+
70
+ @dataclass
71
+ class MigrationConfig:
72
+ """Configuration for workflow migration behavior.
73
+
74
+ Attributes:
75
+ mode: How to handle migrations (prompt, auto, legacy)
76
+ remembered_choices: User's choices for specific workflows
77
+ show_tips: Whether to show migration tips after workflow runs
78
+ last_prompted: Track when workflows were last prompted
79
+ """
80
+
81
+ mode: str = MIGRATION_MODE_PROMPT
82
+ remembered_choices: dict[str, str] = field(default_factory=dict)
83
+ show_tips: bool = True
84
+ last_prompted: dict[str, str] = field(default_factory=dict)
85
+
86
+ @classmethod
87
+ def load(cls) -> "MigrationConfig":
88
+ """Load migration config from .attune/migration.json"""
89
+ config_path = Path.cwd() / ".attune" / "migration.json"
90
+
91
+ if config_path.exists():
92
+ try:
93
+ with config_path.open() as f:
94
+ data = json.load(f)
95
+ return cls(
96
+ mode=data.get("mode", MIGRATION_MODE_PROMPT),
97
+ remembered_choices=data.get("remembered_choices", {}),
98
+ show_tips=data.get("show_tips", True),
99
+ last_prompted=data.get("last_prompted", {}),
100
+ )
101
+ except (json.JSONDecodeError, OSError) as e:
102
+ logger.warning(f"Failed to load migration config: {e}")
103
+
104
+ return cls()
105
+
106
+ def save(self) -> None:
107
+ """Save migration config to .attune/migration.json"""
108
+ config_dir = Path.cwd() / ".attune"
109
+ config_dir.mkdir(exist_ok=True)
110
+ config_path = config_dir / "migration.json"
111
+
112
+ try:
113
+ with config_path.open("w") as f:
114
+ json.dump(
115
+ {
116
+ "mode": self.mode,
117
+ "remembered_choices": self.remembered_choices,
118
+ "show_tips": self.show_tips,
119
+ "last_prompted": self.last_prompted,
120
+ },
121
+ f,
122
+ indent=2,
123
+ )
124
+ except OSError as e:
125
+ logger.warning(f"Failed to save migration config: {e}")
126
+
127
+ def remember_choice(self, old_name: str, choice: str) -> None:
128
+ """Remember user's migration choice for a workflow."""
129
+ self.remembered_choices[old_name] = choice
130
+ self.save()
131
+
132
+
133
+ def is_interactive() -> bool:
134
+ """Check if we're running in an interactive terminal.
135
+
136
+ Returns False in CI environments or when stdin is not a TTY.
137
+ """
138
+ # Check common CI environment variables
139
+ ci_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "CIRCLECI", "TRAVIS"]
140
+ if any(os.getenv(var) for var in ci_vars):
141
+ return False
142
+
143
+ # Check for explicit non-interactive flag
144
+ if os.getenv("ATTUNE_NO_INTERACTIVE"):
145
+ return False
146
+
147
+ # Check if stdin is a TTY
148
+ return sys.stdin.isatty()
149
+
150
+
151
+ def show_migration_dialog(old_name: str, new_name: str, kwargs: dict[str, Any]) -> tuple[str, dict[str, Any], bool]:
152
+ """Show interactive migration dialog to user.
153
+
154
+ Args:
155
+ old_name: The deprecated workflow name
156
+ new_name: The new canonical workflow name
157
+ kwargs: Default kwargs for the new workflow
158
+
159
+ Returns:
160
+ Tuple of (chosen_name, chosen_kwargs, should_remember)
161
+ """
162
+ # Build the new command string
163
+ flag_parts = []
164
+ for key, value in kwargs.items():
165
+ if key.startswith("_"):
166
+ continue
167
+ if isinstance(value, bool) and value:
168
+ flag_parts.append(f"--{key.replace('_', '-')}")
169
+ else:
170
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
171
+
172
+ new_command = f"attune workflow run {new_name}"
173
+ if flag_parts:
174
+ new_command += " " + " ".join(flag_parts)
175
+
176
+ description = WORKFLOW_DESCRIPTIONS.get(new_name, "")
177
+
178
+ # Print dialog
179
+ print()
180
+ print("┌" + "─" * 68 + "┐")
181
+ print("│" + " Workflow Migration ".center(68) + "│")
182
+ print("├" + "─" * 68 + "┤")
183
+ print(f"│ '{old_name}' has been consolidated into '{new_name}'".ljust(69) + "│")
184
+ if description:
185
+ print(f"│ {description[:64]}".ljust(69) + "│")
186
+ print("│" + " " * 68 + "│")
187
+ print("│ New syntax:".ljust(69) + "│")
188
+ print(f"│ {new_command[:62]}".ljust(69) + "│")
189
+ print("│" + " " * 68 + "│")
190
+ print("│ How would you like to proceed?".ljust(69) + "│")
191
+ print("│" + " " * 68 + "│")
192
+ print("│ 1. Use new syntax (recommended)".ljust(69) + "│")
193
+ print("│ 2. Continue with legacy behavior (deprecated)".ljust(69) + "│")
194
+ print("│ 3. Always use new syntax (don't ask again)".ljust(69) + "│")
195
+ print("│ 4. Always use legacy (don't ask again)".ljust(69) + "│")
196
+ print("└" + "─" * 68 + "┘")
197
+ print()
198
+
199
+ while True:
200
+ try:
201
+ choice = input("Enter choice [1-4]: ").strip()
202
+
203
+ if choice == "1":
204
+ return new_name, kwargs, False
205
+ elif choice == "2":
206
+ return old_name, {}, False
207
+ elif choice == "3":
208
+ return new_name, kwargs, True # Remember: use new
209
+ elif choice == "4":
210
+ return old_name, {}, True # Remember: use legacy
211
+ else:
212
+ print("Please enter 1, 2, 3, or 4")
213
+ except (EOFError, KeyboardInterrupt):
214
+ # User cancelled - default to new syntax
215
+ print("\nDefaulting to new syntax...")
216
+ return new_name, kwargs, False
217
+
218
+
219
+ def show_removed_workflow_error(old_name: str, message: str) -> None:
220
+ """Show error for removed workflows with migration guidance."""
221
+ print()
222
+ print("┌" + "─" * 68 + "┐")
223
+ print("│" + " Workflow Removed ".center(68) + "│")
224
+ print("├" + "─" * 68 + "┤")
225
+ print(f"│ '{old_name}' is no longer available.".ljust(69) + "│")
226
+ print("│" + " " * 68 + "│")
227
+ # Word wrap the message
228
+ words = message.split()
229
+ line = "│ "
230
+ for word in words:
231
+ if len(line) + len(word) + 1 > 67:
232
+ print(line.ljust(69) + "│")
233
+ line = "│ " + word + " "
234
+ else:
235
+ line += word + " "
236
+ if line.strip() != "│":
237
+ print(line.ljust(69) + "│")
238
+ print("│" + " " * 68 + "│")
239
+ print("│ Run 'attune workflow list' to see available workflows.".ljust(69) + "│")
240
+ print("└" + "─" * 68 + "┘")
241
+ print()
242
+
243
+
244
+ def show_deprecation_warning(old_name: str, new_name: str, kwargs: dict[str, Any]) -> None:
245
+ """Show deprecation warning (non-blocking)."""
246
+ flag_parts = []
247
+ for key, value in kwargs.items():
248
+ if key.startswith("_"):
249
+ continue
250
+ if isinstance(value, bool) and value:
251
+ flag_parts.append(f"--{key.replace('_', '-')}")
252
+ else:
253
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
254
+
255
+ new_command = f"attune workflow run {new_name}"
256
+ if flag_parts:
257
+ new_command += " " + " ".join(flag_parts)
258
+
259
+ logger.warning(
260
+ f"'{old_name}' is deprecated. Use '{new_command}' instead. "
261
+ f"Set ATTUNE_NO_INTERACTIVE=1 to suppress this warning in CI."
262
+ )
263
+
264
+
265
+ def resolve_workflow_migration(
266
+ workflow_name: str,
267
+ config: MigrationConfig | None = None,
268
+ ) -> tuple[str, dict[str, Any], bool]:
269
+ """Resolve a workflow name, handling migrations as needed.
270
+
271
+ This is the main entry point for the migration system.
272
+
273
+ Args:
274
+ workflow_name: The workflow name from user input
275
+ config: Optional migration config (loads from file if not provided)
276
+
277
+ Returns:
278
+ Tuple of:
279
+ - resolved_name: The canonical workflow name to use
280
+ - kwargs: Additional kwargs to pass to the workflow
281
+ - was_migrated: True if the workflow was migrated
282
+
283
+ Raises:
284
+ SystemExit: If workflow has been removed
285
+ """
286
+ # Not an aliased workflow - return as-is
287
+ if workflow_name not in WORKFLOW_ALIASES:
288
+ return workflow_name, {}, False
289
+
290
+ new_name, kwargs = WORKFLOW_ALIASES[workflow_name]
291
+
292
+ # Handle removed workflows
293
+ if kwargs.get("_removed"):
294
+ message = kwargs.get("_message", "This workflow has been removed.")
295
+ if is_interactive():
296
+ show_removed_workflow_error(workflow_name, message)
297
+ else:
298
+ logger.error(f"Workflow '{workflow_name}' has been removed: {message}")
299
+ raise SystemExit(1)
300
+
301
+ # Load config if not provided
302
+ if config is None:
303
+ config = MigrationConfig.load()
304
+
305
+ # Check if user has a remembered choice
306
+ if workflow_name in config.remembered_choices:
307
+ remembered = config.remembered_choices[workflow_name]
308
+ if remembered == "new":
309
+ logger.debug(f"Using remembered choice: {new_name}")
310
+ return new_name, kwargs, True
311
+ elif remembered == "legacy":
312
+ logger.debug(f"Using remembered choice: {workflow_name} (legacy)")
313
+ return workflow_name, {}, True
314
+
315
+ # Handle based on mode and environment
316
+ is_deprecated = kwargs.get("_deprecated", False)
317
+
318
+ if not is_interactive():
319
+ # CI/non-interactive: silent aliasing with log
320
+ if config.mode == MIGRATION_MODE_LEGACY:
321
+ logger.info(f"Using legacy workflow '{workflow_name}' (migration mode: legacy)")
322
+ return workflow_name, {}, False
323
+ else:
324
+ show_deprecation_warning(workflow_name, new_name, kwargs)
325
+ return new_name, kwargs, True
326
+
327
+ # Interactive mode
328
+ if config.mode == MIGRATION_MODE_AUTO:
329
+ # Auto-migrate without prompting
330
+ show_deprecation_warning(workflow_name, new_name, kwargs)
331
+ return new_name, kwargs, True
332
+
333
+ elif config.mode == MIGRATION_MODE_LEGACY:
334
+ # Use legacy behavior
335
+ if is_deprecated:
336
+ show_deprecation_warning(workflow_name, new_name, kwargs)
337
+ return workflow_name, {}, False
338
+
339
+ else: # MIGRATION_MODE_PROMPT
340
+ # Show interactive dialog
341
+ chosen_name, chosen_kwargs, remember = show_migration_dialog(workflow_name, new_name, kwargs)
342
+
343
+ if remember:
344
+ if chosen_name == new_name:
345
+ config.remember_choice(workflow_name, "new")
346
+ else:
347
+ config.remember_choice(workflow_name, "legacy")
348
+
349
+ was_migrated = chosen_name == new_name
350
+ return chosen_name, chosen_kwargs, was_migrated
351
+
352
+
353
+ def show_migration_tip(old_name: str, new_name: str, kwargs: dict[str, Any]) -> None:
354
+ """Show a migration tip after workflow completion.
355
+
356
+ Called at the end of a workflow run to educate users.
357
+ """
358
+ config = MigrationConfig.load()
359
+ if not config.show_tips:
360
+ return
361
+
362
+ if not is_interactive():
363
+ return
364
+
365
+ # Build command
366
+ flag_parts = []
367
+ for key, value in kwargs.items():
368
+ if key.startswith("_"):
369
+ continue
370
+ if isinstance(value, bool) and value:
371
+ flag_parts.append(f"--{key.replace('_', '-')}")
372
+ else:
373
+ flag_parts.append(f"--{key.replace('_', '-')} {value}")
374
+
375
+ new_command = f"attune workflow run {new_name}"
376
+ if flag_parts:
377
+ new_command += " " + " ".join(flag_parts)
378
+
379
+ print()
380
+ print(f"💡 Tip: '{old_name}' is now '{new_command}'")
381
+ print(" Run 'attune config set workflow.migration.mode auto' to auto-migrate")
382
+ print(" Run 'attune config set workflow.migration.show_tips false' to hide tips")
383
+ print()
384
+
385
+
386
+ def get_canonical_workflow_name(workflow_name: str) -> str:
387
+ """Get the canonical (new) name for a workflow.
388
+
389
+ Useful for documentation and help text.
390
+ """
391
+ if workflow_name in WORKFLOW_ALIASES:
392
+ new_name, _ = WORKFLOW_ALIASES[workflow_name]
393
+ return new_name if new_name else workflow_name
394
+ return workflow_name
395
+
396
+
397
+ def list_migrations() -> list[dict[str, Any]]:
398
+ """List all workflow migrations for documentation.
399
+
400
+ Returns:
401
+ List of migration info dicts
402
+ """
403
+ migrations = []
404
+ for old_name, (new_name, kwargs) in WORKFLOW_ALIASES.items():
405
+ if new_name is None:
406
+ status = "removed"
407
+ elif kwargs.get("_deprecated"):
408
+ status = "deprecated"
409
+ else:
410
+ status = "consolidated"
411
+
412
+ migrations.append(
413
+ {
414
+ "old_name": old_name,
415
+ "new_name": new_name,
416
+ "status": status,
417
+ "kwargs": {k: v for k, v in kwargs.items() if not k.startswith("_")},
418
+ "message": kwargs.get("_message", ""),
419
+ }
420
+ )
421
+
422
+ return migrations
@@ -120,8 +120,7 @@ class WorkflowReport:
120
120
 
121
121
  def _render_rich(self, console: ConsoleType) -> None:
122
122
  """Render report using Rich."""
123
- # Header with score
124
- header_parts = [f"[bold]{self.title}[/bold]"]
123
+ # Score panel (title shown via score panel)
125
124
  if self.score is not None:
126
125
  score_panel = MetricsPanel.render_score(self.score)
127
126
  console.print(score_panel)
@@ -143,9 +142,7 @@ class WorkflowReport:
143
142
  }.get(section.style, "blue")
144
143
 
145
144
  if isinstance(section.content, str):
146
- console.print(
147
- Panel(section.content, title=section.title, border_style=border_style)
148
- )
145
+ console.print(Panel(section.content, title=section.title, border_style=border_style))
149
146
  elif isinstance(section.content, list) and all(
150
147
  isinstance(f, Finding) for f in section.content
151
148
  ):
@@ -326,10 +323,7 @@ class MetricsPanel:
326
323
  Rich Panel with formatted score
327
324
  """
328
325
  if not RICH_AVAILABLE or Panel is None:
329
- raise RuntimeError(
330
- "Rich library not available. "
331
- "Install with: pip install rich"
332
- )
326
+ raise RuntimeError("Rich library not available. " "Install with: pip install rich")
333
327
 
334
328
  style = cls.get_style(score)
335
329
  icon = cls.get_icon(score)