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.
- attune/cli/__init__.py +3 -55
- attune/cli/commands/batch.py +4 -12
- attune/cli/commands/cache.py +7 -15
- attune/cli/commands/provider.py +17 -0
- attune/cli/commands/routing.py +3 -1
- attune/cli/commands/setup.py +122 -0
- attune/cli/commands/tier.py +1 -3
- attune/cli/commands/workflow.py +31 -0
- attune/cli/parsers/cache.py +1 -0
- attune/cli/parsers/help.py +1 -3
- attune/cli/parsers/provider.py +7 -0
- attune/cli/parsers/routing.py +1 -3
- attune/cli/parsers/setup.py +7 -0
- attune/cli/parsers/status.py +1 -3
- attune/cli/parsers/tier.py +1 -3
- attune/cli_minimal.py +34 -28
- attune/cli_router.py +9 -7
- attune/cli_unified.py +3 -0
- attune/core.py +190 -0
- attune/dashboard/app.py +4 -2
- attune/dashboard/simple_server.py +3 -1
- attune/dashboard/standalone_server.py +7 -3
- attune/mcp/server.py +54 -102
- attune/memory/long_term.py +0 -2
- attune/memory/short_term/__init__.py +84 -0
- attune/memory/short_term/base.py +467 -0
- attune/memory/short_term/batch.py +219 -0
- attune/memory/short_term/caching.py +227 -0
- attune/memory/short_term/conflicts.py +265 -0
- attune/memory/short_term/cross_session.py +122 -0
- attune/memory/short_term/facade.py +655 -0
- attune/memory/short_term/pagination.py +215 -0
- attune/memory/short_term/patterns.py +271 -0
- attune/memory/short_term/pubsub.py +286 -0
- attune/memory/short_term/queues.py +244 -0
- attune/memory/short_term/security.py +300 -0
- attune/memory/short_term/sessions.py +250 -0
- attune/memory/short_term/streams.py +249 -0
- attune/memory/short_term/timelines.py +234 -0
- attune/memory/short_term/transactions.py +186 -0
- attune/memory/short_term/working.py +252 -0
- attune/meta_workflows/cli_commands/__init__.py +3 -0
- attune/meta_workflows/cli_commands/agent_commands.py +0 -4
- attune/meta_workflows/cli_commands/analytics_commands.py +0 -6
- attune/meta_workflows/cli_commands/config_commands.py +0 -5
- attune/meta_workflows/cli_commands/memory_commands.py +0 -5
- attune/meta_workflows/cli_commands/template_commands.py +0 -5
- attune/meta_workflows/cli_commands/workflow_commands.py +0 -6
- attune/meta_workflows/workflow.py +1 -1
- attune/models/adaptive_routing.py +4 -8
- attune/models/auth_cli.py +3 -9
- attune/models/auth_strategy.py +2 -4
- attune/models/provider_config.py +20 -1
- attune/models/telemetry/analytics.py +0 -2
- attune/models/telemetry/backend.py +0 -3
- attune/models/telemetry/storage.py +0 -2
- attune/orchestration/_strategies/__init__.py +156 -0
- attune/orchestration/_strategies/base.py +231 -0
- attune/orchestration/_strategies/conditional_strategies.py +373 -0
- attune/orchestration/_strategies/conditions.py +369 -0
- attune/orchestration/_strategies/core_strategies.py +491 -0
- attune/orchestration/_strategies/data_classes.py +64 -0
- attune/orchestration/_strategies/nesting.py +233 -0
- attune/orchestration/execution_strategies.py +58 -1567
- attune/orchestration/meta_orchestrator.py +1 -3
- attune/project_index/scanner.py +1 -3
- attune/project_index/scanner_parallel.py +7 -5
- attune/socratic_router.py +1 -3
- attune/telemetry/agent_coordination.py +9 -3
- attune/telemetry/agent_tracking.py +16 -3
- attune/telemetry/approval_gates.py +22 -5
- attune/telemetry/cli.py +3 -3
- attune/telemetry/commands/dashboard_commands.py +24 -8
- attune/telemetry/event_streaming.py +8 -2
- attune/telemetry/feedback_loop.py +10 -2
- attune/tools.py +1 -0
- attune/workflow_commands.py +1 -3
- attune/workflows/__init__.py +53 -10
- attune/workflows/autonomous_test_gen.py +160 -104
- attune/workflows/base.py +48 -664
- attune/workflows/batch_processing.py +2 -4
- attune/workflows/compat.py +156 -0
- attune/workflows/cost_mixin.py +141 -0
- attune/workflows/data_classes.py +92 -0
- attune/workflows/document_gen/workflow.py +11 -14
- attune/workflows/history.py +62 -37
- attune/workflows/llm_base.py +2 -4
- attune/workflows/migration.py +422 -0
- attune/workflows/output.py +3 -9
- attune/workflows/parsing_mixin.py +427 -0
- attune/workflows/perf_audit.py +3 -1
- attune/workflows/progress.py +10 -13
- attune/workflows/release_prep.py +5 -1
- attune/workflows/routing.py +0 -2
- attune/workflows/secure_release.py +2 -1
- attune/workflows/security_audit.py +19 -14
- attune/workflows/security_audit_phase3.py +28 -22
- attune/workflows/seo_optimization.py +29 -29
- attune/workflows/test_gen/test_templates.py +1 -4
- attune/workflows/test_gen/workflow.py +0 -2
- attune/workflows/test_gen_behavioral.py +7 -20
- attune/workflows/test_gen_parallel.py +6 -4
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/METADATA +4 -3
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/RECORD +119 -94
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/entry_points.txt +0 -2
- attune_healthcare/monitors/monitoring/__init__.py +9 -9
- attune_llm/agent_factory/__init__.py +6 -6
- attune_llm/commands/__init__.py +10 -10
- attune_llm/commands/models.py +3 -3
- attune_llm/config/__init__.py +8 -8
- attune_llm/learning/__init__.py +3 -3
- attune_llm/learning/extractor.py +5 -3
- attune_llm/learning/storage.py +5 -3
- attune_llm/security/__init__.py +17 -17
- attune_llm/utils/tokens.py +3 -1
- attune/cli_legacy.py +0 -3957
- attune/memory/short_term.py +0 -2192
- attune/workflows/manage_docs.py +0 -87
- attune/workflows/test5.py +0 -125
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/WHEEL +0 -0
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {attune_ai-2.1.4.dist-info → attune_ai-2.2.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {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
|
attune/workflows/output.py
CHANGED
|
@@ -120,8 +120,7 @@ class WorkflowReport:
|
|
|
120
120
|
|
|
121
121
|
def _render_rich(self, console: ConsoleType) -> None:
|
|
122
122
|
"""Render report using Rich."""
|
|
123
|
-
#
|
|
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)
|