spec-kitty-cli 0.12.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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""Migration: Complete lane migration, clean up worktrees, and normalize frontmatter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
from ..registry import MigrationRegistry
|
|
11
|
+
from .base import BaseMigration, MigrationResult
|
|
12
|
+
from specify_cli.frontmatter import normalize_file, FrontmatterError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@MigrationRegistry.register
|
|
16
|
+
class CompleteLaneMigration(BaseMigration):
|
|
17
|
+
"""Complete the lane migration and clean up worktrees for v0.9.0+.
|
|
18
|
+
|
|
19
|
+
Part 1: Complete Lane Migration
|
|
20
|
+
The v0.9.0 migration only moved files matching `WP*.md` pattern,
|
|
21
|
+
but some projects have other files in lane subdirectories
|
|
22
|
+
(like phase-*.md, task-*.md, or files without .md extensions).
|
|
23
|
+
|
|
24
|
+
Part 2: Worktree Cleanup
|
|
25
|
+
Worktrees should inherit everything from main repo in v0.9.0+:
|
|
26
|
+
- Agent command directories (.codex/prompts/, .gemini/commands/, etc.)
|
|
27
|
+
- Scripts (.kittify/scripts/)
|
|
28
|
+
Having separate copies causes old command templates to reference
|
|
29
|
+
deprecated scripts like tasks-move-to-lane.sh.
|
|
30
|
+
|
|
31
|
+
Part 3: Frontmatter Normalization (CRITICAL)
|
|
32
|
+
Normalize all YAML frontmatter to absolute consistency using ruamel.yaml.
|
|
33
|
+
This prevents issues where:
|
|
34
|
+
- Some files have `lane: "for_review"` (quoted)
|
|
35
|
+
- Some files have `lane: for_review` (unquoted)
|
|
36
|
+
Both are valid YAML but inconsistency breaks grep searches and tooling.
|
|
37
|
+
|
|
38
|
+
This migration:
|
|
39
|
+
1. Finds ALL remaining files in lane subdirectories (not just WP*.md)
|
|
40
|
+
2. Moves them to the flat tasks/ directory
|
|
41
|
+
3. Ensures lane: field in frontmatter for .md files
|
|
42
|
+
4. Removes any remaining lane subdirectories
|
|
43
|
+
5. Removes ALL agent command directories from worktrees
|
|
44
|
+
6. Removes .kittify/scripts/ from worktrees
|
|
45
|
+
7. Normalizes ALL frontmatter in all .md files for consistency
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
migration_id = "0.9.1_complete_migration"
|
|
49
|
+
description = "Complete lane migration + clean up worktrees + normalize frontmatter"
|
|
50
|
+
target_version = "0.9.1"
|
|
51
|
+
|
|
52
|
+
# All known agent command directories
|
|
53
|
+
AGENT_DIRS = [
|
|
54
|
+
(".claude", "commands"),
|
|
55
|
+
(".github", "prompts"),
|
|
56
|
+
(".gemini", "commands"),
|
|
57
|
+
(".cursor", "commands"),
|
|
58
|
+
(".qwen", "commands"),
|
|
59
|
+
(".opencode", "command"),
|
|
60
|
+
(".windsurf", "workflows"),
|
|
61
|
+
(".codex", "prompts"),
|
|
62
|
+
(".kilocode", "workflows"),
|
|
63
|
+
(".augment", "commands"),
|
|
64
|
+
(".roo", "commands"),
|
|
65
|
+
(".amazonq", "prompts"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
LANE_DIRS: Tuple[str, ...] = ("planned", "doing", "for_review", "done")
|
|
69
|
+
|
|
70
|
+
# System files to ignore when determining if a directory is empty
|
|
71
|
+
# These files are created automatically by operating systems and should not
|
|
72
|
+
# prevent lane directory cleanup
|
|
73
|
+
IGNORE_FILES = frozenset({
|
|
74
|
+
".gitkeep", # Git placeholder
|
|
75
|
+
".DS_Store", # macOS Finder metadata
|
|
76
|
+
"Thumbs.db", # Windows thumbnail cache
|
|
77
|
+
"desktop.ini", # Windows folder settings
|
|
78
|
+
".directory", # KDE folder settings
|
|
79
|
+
"._*", # macOS resource fork prefix (pattern)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def _should_ignore_file(cls, filename: str) -> bool:
|
|
84
|
+
"""Check if a file should be ignored when determining if directory is empty.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
filename: Name of the file to check
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if file should be ignored (system file), False otherwise
|
|
91
|
+
"""
|
|
92
|
+
# Check exact matches
|
|
93
|
+
if filename in cls.IGNORE_FILES:
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
# Check pattern matches (e.g., ._* for macOS resource forks)
|
|
97
|
+
# Check for macOS resource fork files (._filename)
|
|
98
|
+
if filename.startswith("._"):
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _get_real_contents(cls, directory: Path) -> List[Path]:
|
|
105
|
+
"""Get directory contents, excluding system files.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
directory: Path to directory to check
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of "real" files (excluding system files like .DS_Store)
|
|
112
|
+
"""
|
|
113
|
+
if not directory.exists() or not directory.is_dir():
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
item
|
|
118
|
+
for item in directory.iterdir()
|
|
119
|
+
if not cls._should_ignore_file(item.name)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def detect(self, project_path: Path) -> bool:
|
|
123
|
+
"""Check if lane subdirectories exist OR worktrees have agent dirs/scripts."""
|
|
124
|
+
# Part 1: Check for remaining lane subdirectories
|
|
125
|
+
main_specs = project_path / "kitty-specs"
|
|
126
|
+
if main_specs.exists():
|
|
127
|
+
for feature_dir in main_specs.iterdir():
|
|
128
|
+
if feature_dir.is_dir() and self._has_remaining_lane_dirs(feature_dir):
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
worktrees_dir = project_path / ".worktrees"
|
|
132
|
+
if worktrees_dir.exists():
|
|
133
|
+
for worktree in worktrees_dir.iterdir():
|
|
134
|
+
if not worktree.is_dir():
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Check for lane dirs in worktree features
|
|
138
|
+
wt_specs = worktree / "kitty-specs"
|
|
139
|
+
if wt_specs.exists():
|
|
140
|
+
for feature_dir in wt_specs.iterdir():
|
|
141
|
+
if feature_dir.is_dir() and self._has_remaining_lane_dirs(feature_dir):
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
# Part 2: Check for agent command directories in worktree
|
|
145
|
+
for agent_dir, subdir in self.AGENT_DIRS:
|
|
146
|
+
wt_commands = worktree / agent_dir / subdir
|
|
147
|
+
if wt_commands.exists():
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
# Part 2: Check for .kittify/scripts/ in worktree
|
|
151
|
+
wt_scripts = worktree / ".kittify" / "scripts"
|
|
152
|
+
if wt_scripts.exists():
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def _has_remaining_lane_dirs(self, feature_path: Path) -> bool:
|
|
158
|
+
"""Check if feature still has lane subdirectories with any content."""
|
|
159
|
+
tasks_dir = feature_path / "tasks"
|
|
160
|
+
if not tasks_dir.exists():
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
for lane in self.LANE_DIRS:
|
|
164
|
+
lane_path = tasks_dir / lane
|
|
165
|
+
if lane_path.is_dir():
|
|
166
|
+
# Check for real contents (ignoring system files)
|
|
167
|
+
real_contents = self._get_real_contents(lane_path)
|
|
168
|
+
if real_contents:
|
|
169
|
+
return True
|
|
170
|
+
# Even if only system files, still need migration to remove the directory
|
|
171
|
+
# (The directory itself shouldn't exist in new format)
|
|
172
|
+
elif any(lane_path.iterdir()):
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
178
|
+
"""Migration can always be applied if lane directories exist."""
|
|
179
|
+
return True, ""
|
|
180
|
+
|
|
181
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
182
|
+
"""Apply both lane migration and worktree cleanup."""
|
|
183
|
+
changes: List[str] = []
|
|
184
|
+
warnings: List[str] = []
|
|
185
|
+
errors: List[str] = []
|
|
186
|
+
|
|
187
|
+
# Part 1: Complete lane migration
|
|
188
|
+
changes.append("=== Part 1: Complete Lane Migration ===")
|
|
189
|
+
features_found = self._find_features_with_lanes(project_path)
|
|
190
|
+
|
|
191
|
+
if features_found:
|
|
192
|
+
total_migrated = 0
|
|
193
|
+
total_dirs_removed = 0
|
|
194
|
+
|
|
195
|
+
for feature_dir, location_label in features_found:
|
|
196
|
+
feature_changes, feature_warnings, feature_errors, migrated, dirs_removed = (
|
|
197
|
+
self._migrate_remaining_files(feature_dir, location_label, dry_run)
|
|
198
|
+
)
|
|
199
|
+
changes.extend(feature_changes)
|
|
200
|
+
warnings.extend(feature_warnings)
|
|
201
|
+
errors.extend(feature_errors)
|
|
202
|
+
total_migrated += migrated
|
|
203
|
+
total_dirs_removed += dirs_removed
|
|
204
|
+
|
|
205
|
+
if dry_run:
|
|
206
|
+
changes.append(
|
|
207
|
+
f"Would migrate {total_migrated} files and remove {total_dirs_removed} lane directories"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
changes.append(
|
|
211
|
+
f"Migrated {total_migrated} files and removed {total_dirs_removed} lane directories"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
changes.append("No lane subdirectories found")
|
|
215
|
+
|
|
216
|
+
# Part 2: Clean up worktrees
|
|
217
|
+
changes.append("")
|
|
218
|
+
changes.append("=== Part 2: Worktree Cleanup ===")
|
|
219
|
+
worktree_changes, worktree_errors = self._cleanup_worktrees(project_path, dry_run)
|
|
220
|
+
changes.extend(worktree_changes)
|
|
221
|
+
errors.extend(worktree_errors)
|
|
222
|
+
|
|
223
|
+
# Part 3: Normalize frontmatter
|
|
224
|
+
changes.append("")
|
|
225
|
+
changes.append("=== Part 3: Normalize Frontmatter ===")
|
|
226
|
+
fm_changes, fm_warnings, fm_errors = self._normalize_all_frontmatter(project_path, dry_run)
|
|
227
|
+
changes.extend(fm_changes)
|
|
228
|
+
warnings.extend(fm_warnings)
|
|
229
|
+
errors.extend(fm_errors)
|
|
230
|
+
|
|
231
|
+
success = len(errors) == 0
|
|
232
|
+
return MigrationResult(
|
|
233
|
+
success=success,
|
|
234
|
+
changes_made=changes,
|
|
235
|
+
errors=errors,
|
|
236
|
+
warnings=warnings,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _find_features_with_lanes(self, project_path: Path) -> List[Tuple[Path, str]]:
|
|
240
|
+
"""Find all features with remaining lane subdirectories."""
|
|
241
|
+
features: List[Tuple[Path, str]] = []
|
|
242
|
+
|
|
243
|
+
# Scan main kitty-specs/
|
|
244
|
+
main_specs = project_path / "kitty-specs"
|
|
245
|
+
if main_specs.exists():
|
|
246
|
+
for feature_dir in sorted(main_specs.iterdir()):
|
|
247
|
+
if feature_dir.is_dir() and self._has_remaining_lane_dirs(feature_dir):
|
|
248
|
+
features.append((feature_dir, "main"))
|
|
249
|
+
|
|
250
|
+
# Scan .worktrees/
|
|
251
|
+
worktrees_dir = project_path / ".worktrees"
|
|
252
|
+
if worktrees_dir.exists():
|
|
253
|
+
for worktree in sorted(worktrees_dir.iterdir()):
|
|
254
|
+
if worktree.is_dir():
|
|
255
|
+
wt_specs = worktree / "kitty-specs"
|
|
256
|
+
if wt_specs.exists():
|
|
257
|
+
for feature_dir in sorted(wt_specs.iterdir()):
|
|
258
|
+
if feature_dir.is_dir() and self._has_remaining_lane_dirs(feature_dir):
|
|
259
|
+
features.append((feature_dir, f"worktree:{worktree.name}"))
|
|
260
|
+
|
|
261
|
+
return features
|
|
262
|
+
|
|
263
|
+
def _migrate_remaining_files(
|
|
264
|
+
self,
|
|
265
|
+
feature_dir: Path,
|
|
266
|
+
location_label: str,
|
|
267
|
+
dry_run: bool,
|
|
268
|
+
) -> Tuple[List[str], List[str], List[str], int, int]:
|
|
269
|
+
"""Migrate all remaining files from a feature's lane subdirectories."""
|
|
270
|
+
changes: List[str] = []
|
|
271
|
+
warnings: List[str] = []
|
|
272
|
+
errors: List[str] = []
|
|
273
|
+
migrated = 0
|
|
274
|
+
dirs_removed = 0
|
|
275
|
+
|
|
276
|
+
tasks_dir = feature_dir / "tasks"
|
|
277
|
+
if not tasks_dir.exists():
|
|
278
|
+
return changes, warnings, errors, migrated, dirs_removed
|
|
279
|
+
|
|
280
|
+
feature_name = feature_dir.name
|
|
281
|
+
changes.append(f"[{location_label}] {feature_name}:")
|
|
282
|
+
|
|
283
|
+
for lane in self.LANE_DIRS:
|
|
284
|
+
lane_dir = tasks_dir / lane
|
|
285
|
+
if not lane_dir.is_dir():
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Get ALL items in the lane directory (files and subdirectories)
|
|
289
|
+
for item in sorted(lane_dir.iterdir()):
|
|
290
|
+
if item.name == ".gitkeep":
|
|
291
|
+
continue # Skip .gitkeep files
|
|
292
|
+
|
|
293
|
+
if item.is_file():
|
|
294
|
+
# Move file to flat directory
|
|
295
|
+
target = tasks_dir / item.name
|
|
296
|
+
|
|
297
|
+
# Check if already exists
|
|
298
|
+
if target.exists():
|
|
299
|
+
warnings.append(f" Skip: {item.name} already exists in tasks/")
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
if dry_run:
|
|
304
|
+
changes.append(f" Would move: {lane}/{item.name} → tasks/{item.name}")
|
|
305
|
+
else:
|
|
306
|
+
# For .md files, ensure lane in frontmatter
|
|
307
|
+
if item.suffix == ".md":
|
|
308
|
+
content = item.read_text(encoding="utf-8-sig")
|
|
309
|
+
updated_content = self._ensure_lane_in_frontmatter(content, lane)
|
|
310
|
+
target.write_text(updated_content, encoding="utf-8")
|
|
311
|
+
else:
|
|
312
|
+
# For non-.md files, just copy
|
|
313
|
+
target.write_bytes(item.read_bytes())
|
|
314
|
+
|
|
315
|
+
# Remove original
|
|
316
|
+
item.unlink()
|
|
317
|
+
|
|
318
|
+
changes.append(f" Moved: {lane}/{item.name} → tasks/{item.name}")
|
|
319
|
+
|
|
320
|
+
migrated += 1
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
errors.append(f" Error migrating {lane}/{item.name}: {e}")
|
|
324
|
+
|
|
325
|
+
elif item.is_dir():
|
|
326
|
+
# Handle nested directories (shouldn't exist but might)
|
|
327
|
+
warnings.append(
|
|
328
|
+
f" Warning: Nested directory {lane}/{item.name}/ found - please check manually"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Clean up empty lane directory
|
|
332
|
+
if not dry_run:
|
|
333
|
+
if lane_dir.is_dir():
|
|
334
|
+
# Check for real contents (ignoring system files)
|
|
335
|
+
real_contents = self._get_real_contents(lane_dir)
|
|
336
|
+
if not real_contents:
|
|
337
|
+
# Directory has no real files (only system files like .DS_Store or .gitkeep)
|
|
338
|
+
try:
|
|
339
|
+
# Use shutil.rmtree for more robust removal
|
|
340
|
+
# This will remove the directory and all system files within it
|
|
341
|
+
shutil.rmtree(lane_dir)
|
|
342
|
+
changes.append(f" Removed: {lane}/")
|
|
343
|
+
dirs_removed += 1
|
|
344
|
+
except OSError as e:
|
|
345
|
+
warnings.append(f" Could not remove {lane}/: {e}")
|
|
346
|
+
else:
|
|
347
|
+
if lane_dir.is_dir():
|
|
348
|
+
real_contents = self._get_real_contents(lane_dir)
|
|
349
|
+
if not real_contents:
|
|
350
|
+
changes.append(f" Would remove: {lane}/")
|
|
351
|
+
dirs_removed += 1
|
|
352
|
+
|
|
353
|
+
return changes, warnings, errors, migrated, dirs_removed
|
|
354
|
+
|
|
355
|
+
def _ensure_lane_in_frontmatter(self, content: str, expected_lane: str) -> str:
|
|
356
|
+
"""Ensure frontmatter has correct lane field."""
|
|
357
|
+
# Find frontmatter boundaries
|
|
358
|
+
if not content.startswith("---"):
|
|
359
|
+
# No frontmatter, add it
|
|
360
|
+
return f'---\nlane: "{expected_lane}"\n---\n{content}'
|
|
361
|
+
|
|
362
|
+
# Find closing ---
|
|
363
|
+
lines = content.split("\n")
|
|
364
|
+
closing_idx = -1
|
|
365
|
+
for i, line in enumerate(lines[1:], start=1):
|
|
366
|
+
if line.strip() == "---":
|
|
367
|
+
closing_idx = i
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
if closing_idx == -1:
|
|
371
|
+
# Malformed frontmatter, add lane anyway
|
|
372
|
+
return f'---\nlane: "{expected_lane}"\n---\n{content}'
|
|
373
|
+
|
|
374
|
+
frontmatter_lines = lines[1:closing_idx]
|
|
375
|
+
body_lines = lines[closing_idx + 1:]
|
|
376
|
+
|
|
377
|
+
# Check if lane field exists
|
|
378
|
+
lane_pattern = re.compile(r'^lane:\s*(.*)$')
|
|
379
|
+
lane_found = False
|
|
380
|
+
updated_lines = []
|
|
381
|
+
|
|
382
|
+
for line in frontmatter_lines:
|
|
383
|
+
match = lane_pattern.match(line)
|
|
384
|
+
if match:
|
|
385
|
+
lane_found = True
|
|
386
|
+
current_value = match.group(1).strip().strip('"\'')
|
|
387
|
+
if current_value != expected_lane:
|
|
388
|
+
# Replace with expected lane from directory
|
|
389
|
+
updated_lines.append(f'lane: "{expected_lane}"')
|
|
390
|
+
else:
|
|
391
|
+
updated_lines.append(line)
|
|
392
|
+
else:
|
|
393
|
+
updated_lines.append(line)
|
|
394
|
+
|
|
395
|
+
if not lane_found:
|
|
396
|
+
# Insert lane field (before history: if present, otherwise at end)
|
|
397
|
+
insert_idx = len(updated_lines)
|
|
398
|
+
for i, line in enumerate(updated_lines):
|
|
399
|
+
if line.startswith("history:"):
|
|
400
|
+
insert_idx = i
|
|
401
|
+
break
|
|
402
|
+
updated_lines.insert(insert_idx, f'lane: "{expected_lane}"')
|
|
403
|
+
|
|
404
|
+
# Reconstruct document
|
|
405
|
+
result_lines = ["---"] + updated_lines + ["---"] + body_lines
|
|
406
|
+
return "\n".join(result_lines)
|
|
407
|
+
|
|
408
|
+
def _cleanup_worktrees(self, project_path: Path, dry_run: bool) -> Tuple[List[str], List[str]]:
|
|
409
|
+
"""Clean up agent command directories and scripts from all worktrees."""
|
|
410
|
+
changes: List[str] = []
|
|
411
|
+
errors: List[str] = []
|
|
412
|
+
|
|
413
|
+
worktrees_dir = project_path / ".worktrees"
|
|
414
|
+
if not worktrees_dir.exists():
|
|
415
|
+
changes.append("No .worktrees/ directory found")
|
|
416
|
+
return changes, errors
|
|
417
|
+
|
|
418
|
+
worktrees_cleaned = 0
|
|
419
|
+
for worktree in sorted(worktrees_dir.iterdir()):
|
|
420
|
+
if not worktree.is_dir():
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
worktree_name = worktree.name
|
|
424
|
+
cleaned_this_worktree = False
|
|
425
|
+
|
|
426
|
+
# Remove agent command directories
|
|
427
|
+
for agent_dir, subdir in self.AGENT_DIRS:
|
|
428
|
+
commands_dir = worktree / agent_dir / subdir
|
|
429
|
+
# Check is_symlink() FIRST - exists() returns False for broken symlinks!
|
|
430
|
+
if commands_dir.is_symlink() or commands_dir.exists():
|
|
431
|
+
if dry_run:
|
|
432
|
+
is_symlink = commands_dir.is_symlink()
|
|
433
|
+
type_str = "symlink" if is_symlink else "directory"
|
|
434
|
+
changes.append(
|
|
435
|
+
f"[{worktree_name}] Would remove {agent_dir}/{subdir}/ ({type_str})"
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
try:
|
|
439
|
+
# Check if it's a symlink - handle differently
|
|
440
|
+
if commands_dir.is_symlink():
|
|
441
|
+
commands_dir.unlink()
|
|
442
|
+
changes.append(
|
|
443
|
+
f"[{worktree_name}] Removed {agent_dir}/{subdir}/ symlink (inherits from main)"
|
|
444
|
+
)
|
|
445
|
+
elif commands_dir.is_dir():
|
|
446
|
+
shutil.rmtree(commands_dir)
|
|
447
|
+
changes.append(
|
|
448
|
+
f"[{worktree_name}] Removed {agent_dir}/{subdir}/ (inherits from main)"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Clean up parent directory if now empty
|
|
452
|
+
parent = commands_dir.parent
|
|
453
|
+
if parent.exists() and not any(parent.iterdir()):
|
|
454
|
+
parent.rmdir()
|
|
455
|
+
|
|
456
|
+
cleaned_this_worktree = True
|
|
457
|
+
|
|
458
|
+
except OSError as e:
|
|
459
|
+
errors.append(
|
|
460
|
+
f"[{worktree_name}] Failed to remove {agent_dir}/{subdir}/: {e}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# Remove .kittify/scripts/
|
|
464
|
+
scripts_dir = worktree / ".kittify" / "scripts"
|
|
465
|
+
# Check is_symlink() FIRST - exists() returns False for broken symlinks!
|
|
466
|
+
if scripts_dir.is_symlink() or scripts_dir.exists():
|
|
467
|
+
if dry_run:
|
|
468
|
+
is_symlink = scripts_dir.is_symlink()
|
|
469
|
+
type_str = "symlink" if is_symlink else "directory"
|
|
470
|
+
changes.append(
|
|
471
|
+
f"[{worktree_name}] Would remove .kittify/scripts/ ({type_str})"
|
|
472
|
+
)
|
|
473
|
+
else:
|
|
474
|
+
try:
|
|
475
|
+
# Check if it's a symlink - handle differently
|
|
476
|
+
if scripts_dir.is_symlink():
|
|
477
|
+
scripts_dir.unlink()
|
|
478
|
+
changes.append(
|
|
479
|
+
f"[{worktree_name}] Removed .kittify/scripts/ symlink (inherits from main)"
|
|
480
|
+
)
|
|
481
|
+
elif scripts_dir.is_dir():
|
|
482
|
+
shutil.rmtree(scripts_dir)
|
|
483
|
+
changes.append(
|
|
484
|
+
f"[{worktree_name}] Removed .kittify/scripts/ (inherits from main)"
|
|
485
|
+
)
|
|
486
|
+
cleaned_this_worktree = True
|
|
487
|
+
except OSError as e:
|
|
488
|
+
errors.append(
|
|
489
|
+
f"[{worktree_name}] Failed to remove .kittify/scripts/: {e}"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
if cleaned_this_worktree:
|
|
493
|
+
worktrees_cleaned += 1
|
|
494
|
+
|
|
495
|
+
if worktrees_cleaned > 0:
|
|
496
|
+
if dry_run:
|
|
497
|
+
changes.append(f"Would clean up {worktrees_cleaned} worktrees")
|
|
498
|
+
else:
|
|
499
|
+
changes.append(f"Cleaned up {worktrees_cleaned} worktrees")
|
|
500
|
+
else:
|
|
501
|
+
changes.append("No worktrees needed cleanup")
|
|
502
|
+
|
|
503
|
+
return changes, errors
|
|
504
|
+
|
|
505
|
+
def _normalize_all_frontmatter(
|
|
506
|
+
self, project_path: Path, dry_run: bool
|
|
507
|
+
) -> Tuple[List[str], List[str], List[str]]:
|
|
508
|
+
"""Normalize frontmatter in all markdown files for consistency.
|
|
509
|
+
|
|
510
|
+
This ensures:
|
|
511
|
+
- Consistent YAML formatting (no manual quotes)
|
|
512
|
+
- Consistent field ordering
|
|
513
|
+
- Proper ruamel.yaml formatting
|
|
514
|
+
"""
|
|
515
|
+
changes: List[str] = []
|
|
516
|
+
warnings: List[str] = []
|
|
517
|
+
errors: List[str] = []
|
|
518
|
+
|
|
519
|
+
# Find all markdown files in kitty-specs/
|
|
520
|
+
md_files: List[Path] = []
|
|
521
|
+
|
|
522
|
+
# Main kitty-specs/
|
|
523
|
+
main_specs = project_path / "kitty-specs"
|
|
524
|
+
if main_specs.exists():
|
|
525
|
+
md_files.extend(main_specs.rglob("*.md"))
|
|
526
|
+
|
|
527
|
+
# Worktrees
|
|
528
|
+
worktrees_dir = project_path / ".worktrees"
|
|
529
|
+
if worktrees_dir.exists():
|
|
530
|
+
for worktree in worktrees_dir.iterdir():
|
|
531
|
+
if worktree.is_dir():
|
|
532
|
+
wt_specs = worktree / "kitty-specs"
|
|
533
|
+
if wt_specs.exists():
|
|
534
|
+
md_files.extend(wt_specs.rglob("*.md"))
|
|
535
|
+
|
|
536
|
+
if not md_files:
|
|
537
|
+
changes.append("No markdown files found")
|
|
538
|
+
return changes, warnings, errors
|
|
539
|
+
|
|
540
|
+
normalized_count = 0
|
|
541
|
+
skipped_count = 0
|
|
542
|
+
|
|
543
|
+
for md_file in sorted(md_files):
|
|
544
|
+
# Skip if not a task/WP file (e.g., README.md, spec.md, etc.)
|
|
545
|
+
# Only normalize files in tasks/ directories
|
|
546
|
+
if "tasks" not in md_file.parts:
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
if dry_run:
|
|
551
|
+
# Just check if it would change
|
|
552
|
+
try:
|
|
553
|
+
from specify_cli.frontmatter import FrontmatterManager
|
|
554
|
+
manager = FrontmatterManager()
|
|
555
|
+
original = md_file.read_text(encoding="utf-8-sig")
|
|
556
|
+
frontmatter, body = manager.read(md_file)
|
|
557
|
+
|
|
558
|
+
# Write to temp buffer
|
|
559
|
+
import io
|
|
560
|
+
buffer = io.StringIO()
|
|
561
|
+
buffer.write("---\n")
|
|
562
|
+
manager.yaml.dump(manager._normalize_frontmatter(frontmatter), buffer)
|
|
563
|
+
buffer.write("---\n")
|
|
564
|
+
buffer.write(body)
|
|
565
|
+
new_content = buffer.getvalue()
|
|
566
|
+
|
|
567
|
+
if original != new_content:
|
|
568
|
+
changes.append(f"Would normalize: {md_file.relative_to(project_path)}")
|
|
569
|
+
normalized_count += 1
|
|
570
|
+
else:
|
|
571
|
+
skipped_count += 1
|
|
572
|
+
except FrontmatterError:
|
|
573
|
+
warnings.append(f"Skip (no frontmatter): {md_file.relative_to(project_path)}")
|
|
574
|
+
skipped_count += 1
|
|
575
|
+
else:
|
|
576
|
+
# Actually normalize
|
|
577
|
+
if normalize_file(md_file):
|
|
578
|
+
changes.append(f"Normalized: {md_file.relative_to(project_path)}")
|
|
579
|
+
normalized_count += 1
|
|
580
|
+
else:
|
|
581
|
+
skipped_count += 1
|
|
582
|
+
|
|
583
|
+
except FrontmatterError as e:
|
|
584
|
+
warnings.append(f"Skip (error): {md_file.relative_to(project_path)}: {e}")
|
|
585
|
+
skipped_count += 1
|
|
586
|
+
except Exception as e:
|
|
587
|
+
errors.append(f"Failed to normalize {md_file.relative_to(project_path)}: {e}")
|
|
588
|
+
|
|
589
|
+
if dry_run:
|
|
590
|
+
changes.append(f"Would normalize {normalized_count} files, skip {skipped_count}")
|
|
591
|
+
else:
|
|
592
|
+
changes.append(f"Normalized {normalized_count} files, skipped {skipped_count}")
|
|
593
|
+
|
|
594
|
+
return changes, warnings, errors
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# Export AGENT_DIRS for use by other migrations
|
|
598
|
+
# This is the canonical source - all other migrations should import this
|
|
599
|
+
AGENT_DIR_TO_KEY = {
|
|
600
|
+
".claude": "claude",
|
|
601
|
+
".github": "copilot",
|
|
602
|
+
".gemini": "gemini",
|
|
603
|
+
".cursor": "cursor",
|
|
604
|
+
".qwen": "qwen",
|
|
605
|
+
".opencode": "opencode",
|
|
606
|
+
".windsurf": "windsurf",
|
|
607
|
+
".codex": "codex",
|
|
608
|
+
".kilocode": "kilocode",
|
|
609
|
+
".augment": "auggie",
|
|
610
|
+
".roo": "roo",
|
|
611
|
+
".amazonq": "q",
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def get_agent_dirs_for_project(project_path: Path) -> list[tuple[str, str]]:
|
|
616
|
+
"""Get agent directories to process based on project config.
|
|
617
|
+
|
|
618
|
+
Reads config.yaml to determine which agents are enabled.
|
|
619
|
+
Only returns directories for configured agents.
|
|
620
|
+
Falls back to all agents for legacy projects without config.
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
project_path: Path to project root
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
List of (agent_root, subdir) tuples for configured agents
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
from specify_cli.orchestrator.agent_config import get_configured_agents
|
|
630
|
+
|
|
631
|
+
available = get_configured_agents(project_path)
|
|
632
|
+
|
|
633
|
+
if not available:
|
|
634
|
+
# Empty config - fallback to all agents
|
|
635
|
+
return list(CompleteLaneMigration.AGENT_DIRS)
|
|
636
|
+
|
|
637
|
+
# Filter AGENT_DIRS to only include configured agents
|
|
638
|
+
configured_dirs = []
|
|
639
|
+
for agent_root, subdir in CompleteLaneMigration.AGENT_DIRS:
|
|
640
|
+
agent_key = AGENT_DIR_TO_KEY.get(agent_root)
|
|
641
|
+
if agent_key in available:
|
|
642
|
+
configured_dirs.append((agent_root, subdir))
|
|
643
|
+
|
|
644
|
+
return configured_dirs
|
|
645
|
+
|
|
646
|
+
except Exception:
|
|
647
|
+
# Config missing or error reading - fallback to all agents
|
|
648
|
+
# This handles legacy projects gracefully
|
|
649
|
+
return list(CompleteLaneMigration.AGENT_DIRS)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
__all__ = [
|
|
653
|
+
"CompleteLaneMigration",
|
|
654
|
+
"AGENT_DIR_TO_KEY",
|
|
655
|
+
"get_agent_dirs_for_project",
|
|
656
|
+
]
|