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,221 @@
|
|
|
1
|
+
"""Migration: Add missing task-prompt-template.md and tasks.md to research mission.
|
|
2
|
+
|
|
3
|
+
This migration fixes a bug where research missions were missing:
|
|
4
|
+
1. task-prompt-template.md - The YAML frontmatter template for WP files
|
|
5
|
+
2. tasks.md command template - Instructions for generating WP files
|
|
6
|
+
|
|
7
|
+
Without these templates, LLMs generating research WP files created files
|
|
8
|
+
with **Status**: in the markdown body instead of lane: in YAML frontmatter,
|
|
9
|
+
causing the review command to not find WPs ready for review.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ..registry import MigrationRegistry
|
|
18
|
+
from .base import BaseMigration, MigrationResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@MigrationRegistry.register
|
|
22
|
+
class ResearchMissionTemplatesMigration(BaseMigration):
|
|
23
|
+
"""Add missing templates to research mission.
|
|
24
|
+
|
|
25
|
+
This fixes the bug where:
|
|
26
|
+
- Research WP files used **Status**: for_review in markdown body
|
|
27
|
+
- Review command expected lane: "for_review" in YAML frontmatter
|
|
28
|
+
|
|
29
|
+
The fix adds:
|
|
30
|
+
- templates/task-prompt-template.md with proper YAML frontmatter
|
|
31
|
+
- command-templates/tasks.md with instructions to use the template
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
migration_id = "0.9.2_research_mission_templates"
|
|
35
|
+
description = "Add missing task-prompt-template.md and tasks.md to research mission"
|
|
36
|
+
target_version = "0.9.2"
|
|
37
|
+
|
|
38
|
+
# Files that should exist in the research mission
|
|
39
|
+
REQUIRED_FILES = [
|
|
40
|
+
("templates", "task-prompt-template.md"),
|
|
41
|
+
("command-templates", "tasks.md"),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
def detect(self, project_path: Path) -> bool:
|
|
45
|
+
"""Check if research mission is missing required templates."""
|
|
46
|
+
research_mission = project_path / ".kittify" / "missions" / "research"
|
|
47
|
+
|
|
48
|
+
if not research_mission.exists():
|
|
49
|
+
return False # No research mission, nothing to fix
|
|
50
|
+
|
|
51
|
+
for subdir, filename in self.REQUIRED_FILES:
|
|
52
|
+
target = research_mission / subdir / filename
|
|
53
|
+
if not target.exists():
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
59
|
+
"""Check if we can copy templates from the package."""
|
|
60
|
+
package_research = self._find_package_research_mission()
|
|
61
|
+
if package_research is None:
|
|
62
|
+
return (
|
|
63
|
+
False,
|
|
64
|
+
"Could not locate package research mission to copy templates from. "
|
|
65
|
+
"This is expected in test environments. "
|
|
66
|
+
"Run 'spec-kitty init --force' to repair missions manually.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Check that package has the required files
|
|
70
|
+
missing_in_pkg = []
|
|
71
|
+
for subdir, filename in self.REQUIRED_FILES:
|
|
72
|
+
src = package_research / subdir / filename
|
|
73
|
+
if not src.exists():
|
|
74
|
+
missing_in_pkg.append(f"{subdir}/{filename}")
|
|
75
|
+
|
|
76
|
+
if missing_in_pkg:
|
|
77
|
+
return (
|
|
78
|
+
False,
|
|
79
|
+
f"Package research mission is missing: {', '.join(missing_in_pkg)}. "
|
|
80
|
+
"Please upgrade spec-kitty-cli to the latest version.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return True, ""
|
|
84
|
+
|
|
85
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
86
|
+
"""Copy missing templates from the package."""
|
|
87
|
+
changes: list[str] = []
|
|
88
|
+
warnings: list[str] = []
|
|
89
|
+
errors: list[str] = []
|
|
90
|
+
|
|
91
|
+
research_mission = project_path / ".kittify" / "missions" / "research"
|
|
92
|
+
package_research = self._find_package_research_mission()
|
|
93
|
+
|
|
94
|
+
if package_research is None:
|
|
95
|
+
errors.append("Could not locate package research mission")
|
|
96
|
+
return MigrationResult(success=False, errors=errors)
|
|
97
|
+
|
|
98
|
+
if not research_mission.exists():
|
|
99
|
+
# No research mission in project, nothing to do
|
|
100
|
+
return MigrationResult(
|
|
101
|
+
success=True,
|
|
102
|
+
changes_made=["Research mission not present, skipping"],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Copy missing files
|
|
106
|
+
for subdir, filename in self.REQUIRED_FILES:
|
|
107
|
+
src = package_research / subdir / filename
|
|
108
|
+
dest_dir = research_mission / subdir
|
|
109
|
+
dest = dest_dir / filename
|
|
110
|
+
|
|
111
|
+
if dest.exists():
|
|
112
|
+
continue # Already exists
|
|
113
|
+
|
|
114
|
+
if dry_run:
|
|
115
|
+
changes.append(f"Would add research/{subdir}/{filename}")
|
|
116
|
+
else:
|
|
117
|
+
try:
|
|
118
|
+
# Ensure directory exists
|
|
119
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
shutil.copy2(src, dest)
|
|
121
|
+
changes.append(f"Added research/{subdir}/{filename}")
|
|
122
|
+
except OSError as e:
|
|
123
|
+
errors.append(f"Failed to copy {subdir}/{filename}: {e}")
|
|
124
|
+
|
|
125
|
+
# Also update worktrees
|
|
126
|
+
worktrees_dir = project_path / ".worktrees"
|
|
127
|
+
if worktrees_dir.exists():
|
|
128
|
+
for worktree in worktrees_dir.iterdir():
|
|
129
|
+
if not worktree.is_dir():
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
wt_research = worktree / ".kittify" / "missions" / "research"
|
|
133
|
+
if not wt_research.exists():
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
for subdir, filename in self.REQUIRED_FILES:
|
|
137
|
+
src = package_research / subdir / filename
|
|
138
|
+
dest_dir = wt_research / subdir
|
|
139
|
+
dest = dest_dir / filename
|
|
140
|
+
|
|
141
|
+
if dest.exists():
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
if dry_run:
|
|
145
|
+
changes.append(
|
|
146
|
+
f"Would add research/{subdir}/{filename} to worktree {worktree.name}"
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
try:
|
|
150
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
shutil.copy2(src, dest)
|
|
152
|
+
changes.append(
|
|
153
|
+
f"Added research/{subdir}/{filename} to worktree {worktree.name}"
|
|
154
|
+
)
|
|
155
|
+
except OSError as e:
|
|
156
|
+
warnings.append(
|
|
157
|
+
f"Could not copy to worktree {worktree.name}: {e}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
success = len(errors) == 0
|
|
161
|
+
return MigrationResult(
|
|
162
|
+
success=success,
|
|
163
|
+
changes_made=changes,
|
|
164
|
+
errors=errors,
|
|
165
|
+
warnings=warnings,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _find_package_research_mission(self) -> Path | None:
|
|
169
|
+
"""Find the research mission directory in the installed package or local repo."""
|
|
170
|
+
# First try from installed package
|
|
171
|
+
try:
|
|
172
|
+
from importlib.resources import files
|
|
173
|
+
|
|
174
|
+
pkg_files = files("specify_cli")
|
|
175
|
+
missions_path = pkg_files.joinpath("missions", "research")
|
|
176
|
+
|
|
177
|
+
# Convert to Path and check if it exists
|
|
178
|
+
missions_str = str(missions_path)
|
|
179
|
+
if Path(missions_str).exists():
|
|
180
|
+
return Path(missions_str)
|
|
181
|
+
|
|
182
|
+
except (ImportError, TypeError, AttributeError):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Try from package __file__ location
|
|
186
|
+
try:
|
|
187
|
+
import specify_cli
|
|
188
|
+
|
|
189
|
+
pkg_dir = Path(specify_cli.__file__).parent
|
|
190
|
+
research_dir = pkg_dir / "missions" / "research"
|
|
191
|
+
if research_dir.exists():
|
|
192
|
+
return research_dir
|
|
193
|
+
except (ImportError, AttributeError):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# Fallback for development: Check SPEC_KITTY_TEMPLATE_ROOT env var
|
|
197
|
+
import os
|
|
198
|
+
|
|
199
|
+
template_root = os.environ.get("SPEC_KITTY_TEMPLATE_ROOT")
|
|
200
|
+
if template_root:
|
|
201
|
+
research_dir = Path(template_root) / ".kittify" / "missions" / "research"
|
|
202
|
+
if research_dir.exists():
|
|
203
|
+
return research_dir
|
|
204
|
+
|
|
205
|
+
# Fallback: Try to find the spec-kitty repo root
|
|
206
|
+
try:
|
|
207
|
+
cwd = Path.cwd()
|
|
208
|
+
for parent in [cwd] + list(cwd.parents):
|
|
209
|
+
research_dir = parent / "src" / "specify_cli" / "missions" / "research"
|
|
210
|
+
pyproject = parent / "pyproject.toml"
|
|
211
|
+
if research_dir.exists() and pyproject.exists():
|
|
212
|
+
try:
|
|
213
|
+
content = pyproject.read_text(encoding="utf-8-sig")
|
|
214
|
+
if "spec-kitty-cli" in content:
|
|
215
|
+
return research_dir
|
|
216
|
+
except OSError:
|
|
217
|
+
pass
|
|
218
|
+
except OSError:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
return None
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Migration registry for Spec Kitty upgrade system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Dict, List, Type
|
|
6
|
+
|
|
7
|
+
from packaging.version import Version
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .migrations.base import BaseMigration
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MigrationRegistry:
|
|
14
|
+
"""Registry of all available migrations, ordered by target version."""
|
|
15
|
+
|
|
16
|
+
_migrations: Dict[str, Type["BaseMigration"]] = {}
|
|
17
|
+
|
|
18
|
+
# Required fields for all migrations
|
|
19
|
+
REQUIRED_FIELDS = ['migration_id', 'description', 'target_version']
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def register(
|
|
23
|
+
cls, migration_class: Type["BaseMigration"]
|
|
24
|
+
) -> Type["BaseMigration"]:
|
|
25
|
+
"""Decorator to register a migration class.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
migration_class: The migration class to register
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The same migration class (for decorator use)
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If migration_id is not set, required fields are missing,
|
|
35
|
+
or a migration with this ID is already registered
|
|
36
|
+
"""
|
|
37
|
+
# Validate required fields
|
|
38
|
+
for field in cls.REQUIRED_FIELDS:
|
|
39
|
+
value = getattr(migration_class, field, None)
|
|
40
|
+
if not value:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Migration {migration_class.__name__} is missing required field '{field}'"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
migration_id = migration_class.migration_id
|
|
46
|
+
|
|
47
|
+
# Check for duplicate registration
|
|
48
|
+
if migration_id in cls._migrations:
|
|
49
|
+
existing = cls._migrations[migration_id]
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Duplicate migration ID '{migration_id}'. "
|
|
52
|
+
f"Already registered by {existing.__name__}, "
|
|
53
|
+
f"cannot register {migration_class.__name__}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cls._migrations[migration_id] = migration_class
|
|
57
|
+
return migration_class
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_all(cls) -> List["BaseMigration"]:
|
|
61
|
+
"""Get all migrations as instances, ordered by target version.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of migration instances sorted by target version
|
|
65
|
+
"""
|
|
66
|
+
instances = [m() for m in cls._migrations.values()]
|
|
67
|
+
return sorted(instances, key=lambda m: Version(m.target_version))
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_applicable(
|
|
71
|
+
cls, from_version: str, to_version: str, project_path: "Path | None" = None
|
|
72
|
+
) -> List["BaseMigration"]:
|
|
73
|
+
"""Get migrations needed to go from one version to another.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
from_version: Current version
|
|
77
|
+
to_version: Target version
|
|
78
|
+
project_path: Optional project path for detect() check
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of applicable migrations in order
|
|
82
|
+
"""
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
from_v = Version(from_version)
|
|
85
|
+
to_v = Version(to_version)
|
|
86
|
+
|
|
87
|
+
applicable = []
|
|
88
|
+
for migration in cls.get_all():
|
|
89
|
+
target = Version(migration.target_version)
|
|
90
|
+
# Include if target is > from_version AND <= to_version
|
|
91
|
+
if from_v < target <= to_v:
|
|
92
|
+
applicable.append(migration)
|
|
93
|
+
# ALSO include migrations at current version if detect() returns True
|
|
94
|
+
elif target == from_v and project_path is not None:
|
|
95
|
+
if migration.detect(Path(project_path) if isinstance(project_path, str) else project_path):
|
|
96
|
+
applicable.append(migration)
|
|
97
|
+
|
|
98
|
+
return applicable
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_by_id(cls, migration_id: str) -> "BaseMigration | None":
|
|
102
|
+
"""Get a specific migration by ID.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
migration_id: The migration ID to look up
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Migration instance if found, None otherwise
|
|
109
|
+
"""
|
|
110
|
+
migration_class = cls._migrations.get(migration_id)
|
|
111
|
+
return migration_class() if migration_class else None
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def clear(cls) -> None:
|
|
115
|
+
"""Clear all registered migrations (for testing)."""
|
|
116
|
+
cls._migrations.clear()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Export standalone decorator for convenience
|
|
120
|
+
# This allows: from specify_cli.upgrade.registry import register
|
|
121
|
+
register = MigrationRegistry.register
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Migration runner for Spec Kitty upgrade system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from .detector import VersionDetector
|
|
15
|
+
from .metadata import ProjectMetadata
|
|
16
|
+
from .migrations.base import BaseMigration, MigrationResult
|
|
17
|
+
from .registry import MigrationRegistry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class UpgradeResult:
|
|
22
|
+
"""Result of an upgrade operation."""
|
|
23
|
+
|
|
24
|
+
success: bool
|
|
25
|
+
from_version: str
|
|
26
|
+
to_version: str
|
|
27
|
+
migrations_applied: List[str] = field(default_factory=list)
|
|
28
|
+
migrations_skipped: List[str] = field(default_factory=list)
|
|
29
|
+
errors: List[str] = field(default_factory=list)
|
|
30
|
+
warnings: List[str] = field(default_factory=list)
|
|
31
|
+
dry_run: bool = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MigrationRunner:
|
|
35
|
+
"""Orchestrates the migration process."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, project_path: Path, console: Optional[Console] = None):
|
|
38
|
+
"""Initialize the runner.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
project_path: Root of the project
|
|
42
|
+
console: Optional Rich console for output
|
|
43
|
+
"""
|
|
44
|
+
self.project_path = project_path
|
|
45
|
+
self.kittify_dir = project_path / ".kittify"
|
|
46
|
+
self.console = console or Console()
|
|
47
|
+
self.detector = VersionDetector(project_path)
|
|
48
|
+
|
|
49
|
+
def upgrade(
|
|
50
|
+
self,
|
|
51
|
+
target_version: str,
|
|
52
|
+
dry_run: bool = False,
|
|
53
|
+
force: bool = False,
|
|
54
|
+
include_worktrees: bool = True,
|
|
55
|
+
) -> UpgradeResult:
|
|
56
|
+
"""Run all needed migrations to reach target version.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
target_version: Version to upgrade to
|
|
60
|
+
dry_run: If True, simulate but don't apply
|
|
61
|
+
force: If True, skip confirmation prompts
|
|
62
|
+
include_worktrees: If True, also upgrade worktrees
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
UpgradeResult with details of the upgrade
|
|
66
|
+
"""
|
|
67
|
+
from_version = self.detector.detect_version()
|
|
68
|
+
|
|
69
|
+
result = UpgradeResult(
|
|
70
|
+
success=True,
|
|
71
|
+
from_version=from_version,
|
|
72
|
+
to_version=target_version,
|
|
73
|
+
dry_run=dry_run,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Get applicable migrations
|
|
77
|
+
migrations = MigrationRegistry.get_applicable(from_version, target_version, project_path=self.project_path)
|
|
78
|
+
|
|
79
|
+
if not migrations:
|
|
80
|
+
result.warnings.append(
|
|
81
|
+
f"No migrations needed from {from_version} to {target_version}"
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
# Load or create metadata
|
|
86
|
+
metadata = ProjectMetadata.load(self.kittify_dir)
|
|
87
|
+
if metadata is None:
|
|
88
|
+
metadata = self._create_initial_metadata(from_version)
|
|
89
|
+
|
|
90
|
+
# Apply each migration to main project
|
|
91
|
+
for migration in migrations:
|
|
92
|
+
migration_result = self._apply_migration(migration, metadata, dry_run)
|
|
93
|
+
|
|
94
|
+
if migration_result.success:
|
|
95
|
+
result.migrations_applied.append(migration.migration_id)
|
|
96
|
+
result.warnings.extend(migration_result.warnings)
|
|
97
|
+
else:
|
|
98
|
+
# Check if it was skipped (already applied)
|
|
99
|
+
if metadata.has_migration(migration.migration_id):
|
|
100
|
+
result.migrations_skipped.append(migration.migration_id)
|
|
101
|
+
else:
|
|
102
|
+
result.success = False
|
|
103
|
+
result.errors.extend(migration_result.errors)
|
|
104
|
+
# Stop on first failure
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
# Update and save metadata for main project
|
|
108
|
+
if not dry_run and result.success:
|
|
109
|
+
metadata.version = target_version
|
|
110
|
+
metadata.last_upgraded_at = datetime.now()
|
|
111
|
+
metadata.save(self.kittify_dir)
|
|
112
|
+
|
|
113
|
+
# Handle worktrees
|
|
114
|
+
if include_worktrees:
|
|
115
|
+
worktrees_result = self._upgrade_worktrees(
|
|
116
|
+
target_version, migrations, dry_run
|
|
117
|
+
)
|
|
118
|
+
result.warnings.extend(worktrees_result.get("warnings", []))
|
|
119
|
+
if worktrees_result.get("errors"):
|
|
120
|
+
result.errors.extend(worktrees_result["errors"])
|
|
121
|
+
# Don't fail the whole upgrade for worktree issues
|
|
122
|
+
result.warnings.append(
|
|
123
|
+
"Some worktrees had issues - check errors above"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
def _apply_migration(
|
|
129
|
+
self,
|
|
130
|
+
migration: BaseMigration,
|
|
131
|
+
metadata: ProjectMetadata,
|
|
132
|
+
dry_run: bool,
|
|
133
|
+
) -> MigrationResult:
|
|
134
|
+
"""Apply a single migration.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
migration: The migration to apply
|
|
138
|
+
metadata: Project metadata
|
|
139
|
+
dry_run: Whether to simulate only
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
MigrationResult with details
|
|
143
|
+
"""
|
|
144
|
+
# Skip if already applied
|
|
145
|
+
if metadata.has_migration(migration.migration_id):
|
|
146
|
+
return MigrationResult(
|
|
147
|
+
success=True,
|
|
148
|
+
warnings=[f"Migration {migration.migration_id} already applied, skipping"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Check if migration is needed via detection
|
|
152
|
+
if not migration.detect(self.project_path):
|
|
153
|
+
# Migration not needed - project doesn't have old state
|
|
154
|
+
if not dry_run:
|
|
155
|
+
metadata.record_migration(
|
|
156
|
+
migration.migration_id, "skipped", "Not applicable"
|
|
157
|
+
)
|
|
158
|
+
return MigrationResult(
|
|
159
|
+
success=True,
|
|
160
|
+
warnings=[
|
|
161
|
+
f"Migration {migration.migration_id} not needed (project already in target state)"
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Check if safe to apply
|
|
166
|
+
can_apply, reason = migration.can_apply(self.project_path)
|
|
167
|
+
if not can_apply:
|
|
168
|
+
return MigrationResult(
|
|
169
|
+
success=False,
|
|
170
|
+
errors=[f"Cannot apply {migration.migration_id}: {reason}"],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Apply the migration
|
|
174
|
+
result = migration.apply(self.project_path, dry_run=dry_run)
|
|
175
|
+
|
|
176
|
+
# Record in metadata
|
|
177
|
+
if not dry_run:
|
|
178
|
+
metadata.record_migration(
|
|
179
|
+
migration.migration_id,
|
|
180
|
+
"success" if result.success else "failed",
|
|
181
|
+
"; ".join(result.changes_made) if result.changes_made else None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
def _upgrade_worktrees(
|
|
187
|
+
self,
|
|
188
|
+
target_version: str,
|
|
189
|
+
migrations: List[BaseMigration],
|
|
190
|
+
dry_run: bool,
|
|
191
|
+
) -> dict:
|
|
192
|
+
"""Upgrade all worktrees in .worktrees/ directory.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
target_version: Target version
|
|
196
|
+
migrations: List of migrations to apply
|
|
197
|
+
dry_run: Whether to simulate only
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dict with warnings and errors lists
|
|
201
|
+
"""
|
|
202
|
+
result: dict = {"warnings": [], "errors": []}
|
|
203
|
+
|
|
204
|
+
worktrees_dir = self.project_path / ".worktrees"
|
|
205
|
+
if not worktrees_dir.exists():
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
for worktree in worktrees_dir.iterdir():
|
|
209
|
+
if not worktree.is_dir():
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
wt_kittify = worktree / ".kittify"
|
|
213
|
+
if not wt_kittify.exists():
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Load or create worktree metadata
|
|
217
|
+
wt_metadata = ProjectMetadata.load(wt_kittify)
|
|
218
|
+
if wt_metadata is None:
|
|
219
|
+
wt_detector = VersionDetector(worktree)
|
|
220
|
+
wt_version = wt_detector.detect_version()
|
|
221
|
+
wt_metadata = self._create_initial_metadata(wt_version)
|
|
222
|
+
|
|
223
|
+
# Apply migrations to worktree
|
|
224
|
+
for migration in migrations:
|
|
225
|
+
if wt_metadata.has_migration(migration.migration_id):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if not migration.detect(worktree):
|
|
229
|
+
if not dry_run:
|
|
230
|
+
wt_metadata.record_migration(
|
|
231
|
+
migration.migration_id, "skipped", "Not applicable"
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
can_apply, reason = migration.can_apply(worktree)
|
|
236
|
+
if not can_apply:
|
|
237
|
+
result["warnings"].append(
|
|
238
|
+
f"Worktree {worktree.name}: Cannot apply {migration.migration_id}: {reason}"
|
|
239
|
+
)
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
migration_result = migration.apply(worktree, dry_run=dry_run)
|
|
243
|
+
|
|
244
|
+
if migration_result.success:
|
|
245
|
+
if not dry_run:
|
|
246
|
+
wt_metadata.record_migration(
|
|
247
|
+
migration.migration_id,
|
|
248
|
+
"success",
|
|
249
|
+
"; ".join(migration_result.changes_made)
|
|
250
|
+
if migration_result.changes_made
|
|
251
|
+
else None,
|
|
252
|
+
)
|
|
253
|
+
result["warnings"].extend(
|
|
254
|
+
[f"Worktree {worktree.name}: {w}" for w in migration_result.warnings]
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
result["errors"].extend(
|
|
258
|
+
[f"Worktree {worktree.name}: {e}" for e in migration_result.errors]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Save worktree metadata
|
|
262
|
+
if not dry_run:
|
|
263
|
+
wt_metadata.version = target_version
|
|
264
|
+
wt_metadata.last_upgraded_at = datetime.now()
|
|
265
|
+
wt_metadata.save(wt_kittify)
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
def _create_initial_metadata(self, detected_version: str) -> ProjectMetadata:
|
|
270
|
+
"""Create initial metadata for a project without it.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
detected_version: Version detected from heuristics
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
New ProjectMetadata instance
|
|
277
|
+
"""
|
|
278
|
+
return ProjectMetadata(
|
|
279
|
+
version=detected_version,
|
|
280
|
+
initialized_at=datetime.now(),
|
|
281
|
+
python_version=platform.python_version(),
|
|
282
|
+
platform=sys.platform,
|
|
283
|
+
platform_version=platform.platform(),
|
|
284
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Validation helpers for Spec Kitty missions.
|
|
2
|
+
|
|
3
|
+
This package hosts mission-specific validators that keep artifacts such
|
|
4
|
+
as CSV trackers and path conventions consistent. Modules included:
|
|
5
|
+
|
|
6
|
+
- ``research`` – citation + bibliography validation for research mission
|
|
7
|
+
- ``paths`` – (placeholder) path convention validation shared by missions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from . import paths, research
|
|
13
|
+
|
|
14
|
+
__all__ = ["paths", "research"]
|