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,114 @@
|
|
|
1
|
+
"""Migration: Ensure workflow commands in agent prompts include --agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ..registry import MigrationRegistry
|
|
9
|
+
from .base import BaseMigration, MigrationResult
|
|
10
|
+
from .m_0_9_1_complete_lane_migration import get_agent_dirs_for_project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@MigrationRegistry.register
|
|
14
|
+
class WorkflowAgentFlagMigration(BaseMigration):
|
|
15
|
+
"""Append --agent <name> to workflow commands in agent prompts."""
|
|
16
|
+
|
|
17
|
+
migration_id = "0.11.3_workflow_agent_flag"
|
|
18
|
+
description = "Ensure workflow commands in agent prompts include --agent"
|
|
19
|
+
target_version = "0.11.3"
|
|
20
|
+
|
|
21
|
+
AGENT_NAME_MAP = {
|
|
22
|
+
".github": "copilot",
|
|
23
|
+
".augment": "auggie",
|
|
24
|
+
".amazonq": "q",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
TARGET_FILES = ("spec-kitty.implement.md", "spec-kitty.review.md")
|
|
28
|
+
|
|
29
|
+
def _agent_name(self, agent_root: str) -> str:
|
|
30
|
+
return self.AGENT_NAME_MAP.get(agent_root, agent_root.lstrip("."))
|
|
31
|
+
|
|
32
|
+
def _update_workflow_lines(self, path: Path, agent_name: str, dry_run: bool) -> bool:
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
text = path.read_text(encoding="utf-8")
|
|
37
|
+
lines = text.splitlines()
|
|
38
|
+
updated = False
|
|
39
|
+
|
|
40
|
+
def _patch_line(line: str) -> str:
|
|
41
|
+
nonlocal updated
|
|
42
|
+
# Replace __AGENT__ placeholder with actual agent name
|
|
43
|
+
if "__AGENT__" in line:
|
|
44
|
+
updated = True
|
|
45
|
+
line = line.replace("__AGENT__", agent_name)
|
|
46
|
+
# Add --agent flag if missing
|
|
47
|
+
if "spec-kitty agent workflow implement" in line and "--agent" not in line:
|
|
48
|
+
updated = True
|
|
49
|
+
return f"{line} --agent {agent_name}"
|
|
50
|
+
if "spec-kitty agent workflow review" in line and "--agent" not in line:
|
|
51
|
+
updated = True
|
|
52
|
+
return f"{line} --agent {agent_name}"
|
|
53
|
+
return line
|
|
54
|
+
|
|
55
|
+
lines = [_patch_line(line) for line in lines]
|
|
56
|
+
if updated and not dry_run:
|
|
57
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
58
|
+
return updated
|
|
59
|
+
|
|
60
|
+
def detect(self, project_path: Path) -> bool:
|
|
61
|
+
agent_dirs = get_agent_dirs_for_project(project_path)
|
|
62
|
+
for agent_root, subdir in agent_dirs:
|
|
63
|
+
agent_dir = project_path / agent_root / subdir
|
|
64
|
+
if not agent_dir.exists():
|
|
65
|
+
continue
|
|
66
|
+
agent_name = self._agent_name(agent_root)
|
|
67
|
+
for filename in self.TARGET_FILES:
|
|
68
|
+
path = agent_dir / filename
|
|
69
|
+
if not path.exists():
|
|
70
|
+
continue
|
|
71
|
+
text = path.read_text(encoding="utf-8")
|
|
72
|
+
for line in text.splitlines():
|
|
73
|
+
# Detect __AGENT__ placeholder that needs replacement
|
|
74
|
+
if "__AGENT__" in line:
|
|
75
|
+
return True
|
|
76
|
+
if "spec-kitty agent workflow implement" in line and "--agent" not in line:
|
|
77
|
+
return True
|
|
78
|
+
if "spec-kitty agent workflow review" in line and "--agent" not in line:
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
83
|
+
if not (project_path / ".kittify").exists():
|
|
84
|
+
return False, "No .kittify directory (not a spec-kitty project)"
|
|
85
|
+
return True, ""
|
|
86
|
+
|
|
87
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
88
|
+
changes: List[str] = []
|
|
89
|
+
warnings: List[str] = []
|
|
90
|
+
errors: List[str] = []
|
|
91
|
+
|
|
92
|
+
agent_dirs = get_agent_dirs_for_project(project_path)
|
|
93
|
+
for agent_root, subdir in agent_dirs:
|
|
94
|
+
agent_dir = project_path / agent_root / subdir
|
|
95
|
+
if not agent_dir.exists():
|
|
96
|
+
continue
|
|
97
|
+
agent_name = self._agent_name(agent_root)
|
|
98
|
+
updated_count = 0
|
|
99
|
+
for filename in self.TARGET_FILES:
|
|
100
|
+
path = agent_dir / filename
|
|
101
|
+
if self._update_workflow_lines(path, agent_name, dry_run):
|
|
102
|
+
updated_count += 1
|
|
103
|
+
if updated_count:
|
|
104
|
+
changes.append(f"Updated {updated_count} workflow prompts for {agent_name}")
|
|
105
|
+
|
|
106
|
+
if not changes:
|
|
107
|
+
warnings.append("No workflow prompts required updates")
|
|
108
|
+
|
|
109
|
+
return MigrationResult(
|
|
110
|
+
success=len(errors) == 0,
|
|
111
|
+
changes_made=changes,
|
|
112
|
+
errors=errors,
|
|
113
|
+
warnings=warnings,
|
|
114
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Migration: Install documentation mission to user projects (v0.12.0)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ..registry import MigrationRegistry
|
|
10
|
+
from .base import BaseMigration, MigrationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@MigrationRegistry.register
|
|
14
|
+
class InstallDocumentationMission(BaseMigration):
|
|
15
|
+
"""Install the documentation mission to user projects.
|
|
16
|
+
|
|
17
|
+
This migration copies the documentation mission from the spec-kitty
|
|
18
|
+
installation (src/specify_cli/missions/documentation/) to the user's
|
|
19
|
+
project (.kittify/missions/documentation/).
|
|
20
|
+
|
|
21
|
+
The documentation mission enables users to create and maintain software
|
|
22
|
+
documentation following Write the Docs and Divio principles.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
migration_id = "0.12.0_documentation_mission"
|
|
26
|
+
description = "Install documentation mission to user projects"
|
|
27
|
+
target_version = "0.12.0"
|
|
28
|
+
|
|
29
|
+
def detect(self, project_path: Path) -> bool:
|
|
30
|
+
"""Detect if documentation mission needs to be installed.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project_path: Root directory of user's spec-kitty project
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if documentation mission is missing, False if already installed
|
|
37
|
+
"""
|
|
38
|
+
kittify_dir = project_path / ".kittify"
|
|
39
|
+
|
|
40
|
+
if not kittify_dir.exists():
|
|
41
|
+
# Not a spec-kitty project, migration doesn't apply
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
missions_dir = kittify_dir / "missions"
|
|
45
|
+
|
|
46
|
+
if not missions_dir.exists():
|
|
47
|
+
# Missions directory doesn't exist (very old project)
|
|
48
|
+
# Migration should run to create it
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
doc_mission_dir = missions_dir / "documentation"
|
|
52
|
+
|
|
53
|
+
# Check if documentation mission already exists
|
|
54
|
+
if doc_mission_dir.exists() and (doc_mission_dir / "mission.yaml").exists():
|
|
55
|
+
# Already installed
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Documentation mission is missing, migration should run
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
62
|
+
"""Check if migration can be safely applied.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
project_path: Root of the project
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
(can_apply, reason) - True if safe, False with explanation if not
|
|
69
|
+
"""
|
|
70
|
+
# Check if source mission exists
|
|
71
|
+
source_mission = self._find_source_mission()
|
|
72
|
+
if source_mission is None:
|
|
73
|
+
return (
|
|
74
|
+
False,
|
|
75
|
+
"Could not locate documentation mission source in spec-kitty installation. "
|
|
76
|
+
"This may indicate an incomplete installation.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return True, ""
|
|
80
|
+
|
|
81
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
82
|
+
"""Copy documentation mission to user project.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
project_path: Root directory of user's spec-kitty project
|
|
86
|
+
dry_run: If True, only simulate changes
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
MigrationResult indicating success or failure
|
|
90
|
+
"""
|
|
91
|
+
changes: list[str] = []
|
|
92
|
+
errors: list[str] = []
|
|
93
|
+
|
|
94
|
+
kittify_dir = project_path / ".kittify"
|
|
95
|
+
missions_dir = kittify_dir / "missions"
|
|
96
|
+
|
|
97
|
+
# Find source documentation mission
|
|
98
|
+
source_mission = self._find_source_mission()
|
|
99
|
+
|
|
100
|
+
if source_mission is None:
|
|
101
|
+
errors.append("Could not find documentation mission source in spec-kitty installation")
|
|
102
|
+
return MigrationResult(success=False, errors=errors)
|
|
103
|
+
|
|
104
|
+
# Destination
|
|
105
|
+
dest_mission = missions_dir / "documentation"
|
|
106
|
+
|
|
107
|
+
# Check if destination already exists
|
|
108
|
+
if dest_mission.exists() and (dest_mission / "mission.yaml").exists():
|
|
109
|
+
return MigrationResult(
|
|
110
|
+
success=True,
|
|
111
|
+
changes_made=["Documentation mission already installed (skipped)"]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Ensure missions directory exists
|
|
115
|
+
if not missions_dir.exists():
|
|
116
|
+
if dry_run:
|
|
117
|
+
changes.append("Would create .kittify/missions/ directory")
|
|
118
|
+
else:
|
|
119
|
+
missions_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
changes.append("Created .kittify/missions/ directory")
|
|
121
|
+
|
|
122
|
+
# Copy mission directory
|
|
123
|
+
if dry_run:
|
|
124
|
+
changes.append("Would copy documentation mission to .kittify/missions/documentation/")
|
|
125
|
+
else:
|
|
126
|
+
try:
|
|
127
|
+
shutil.copytree(source_mission, dest_mission)
|
|
128
|
+
|
|
129
|
+
# Count copied files for reporting
|
|
130
|
+
copied_files = list(dest_mission.rglob("*"))
|
|
131
|
+
file_count = len([f for f in copied_files if f.is_file()])
|
|
132
|
+
|
|
133
|
+
changes.append(f"Copied documentation mission ({file_count} files)")
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
errors.append(f"Failed to copy documentation mission: {e}")
|
|
137
|
+
return MigrationResult(success=False, errors=errors)
|
|
138
|
+
|
|
139
|
+
return MigrationResult(success=True, changes_made=changes)
|
|
140
|
+
|
|
141
|
+
def _find_source_mission(self) -> Optional[Path]:
|
|
142
|
+
"""Find the documentation mission in spec-kitty's installation.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Path to source mission directory, or None if not found
|
|
146
|
+
"""
|
|
147
|
+
# The source is relative to this migration file
|
|
148
|
+
migrations_dir = Path(__file__).parent
|
|
149
|
+
src_dir = migrations_dir.parent.parent # Up to src/specify_cli/
|
|
150
|
+
source_mission = src_dir / "missions" / "documentation"
|
|
151
|
+
|
|
152
|
+
if source_mission.exists() and (source_mission / "mission.yaml").exists():
|
|
153
|
+
return source_mission
|
|
154
|
+
|
|
155
|
+
return None
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Migration: Remove kitty-specs/ from main repo .gitignore.
|
|
2
|
+
|
|
3
|
+
Historical context:
|
|
4
|
+
- Earlier spec-kitty versions or user templates may have added `kitty-specs/` to .gitignore
|
|
5
|
+
- This prevents git from tracking feature specifications, causing failures in:
|
|
6
|
+
- `spec-kitty agent feature create-feature`
|
|
7
|
+
- `/spec-kitty.specify` (commit step)
|
|
8
|
+
- Other commands that commit to kitty-specs/
|
|
9
|
+
|
|
10
|
+
The fix:
|
|
11
|
+
- Remove `kitty-specs/` entries from main repo .gitignore
|
|
12
|
+
- Keep worktree-specific patterns like `kitty-specs/**/tasks/*.md` (those prevent merge conflicts)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import List, Tuple
|
|
20
|
+
|
|
21
|
+
from ..registry import MigrationRegistry
|
|
22
|
+
from .base import BaseMigration, MigrationResult
|
|
23
|
+
|
|
24
|
+
MIGRATION_ID = "0.12.1_remove_kitty_specs_from_gitignore"
|
|
25
|
+
MIGRATION_VERSION = "0.12.1"
|
|
26
|
+
MIGRATION_DESCRIPTION = "Remove kitty-specs/ from main repo .gitignore to allow tracking feature specs"
|
|
27
|
+
|
|
28
|
+
# Patterns to REMOVE (block entire kitty-specs directory)
|
|
29
|
+
PATTERNS_TO_REMOVE = [
|
|
30
|
+
r"^kitty-specs/?$", # kitty-specs or kitty-specs/
|
|
31
|
+
r"^/kitty-specs/?$", # /kitty-specs or /kitty-specs/
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Patterns to KEEP (worktree-specific, prevent merge conflicts)
|
|
35
|
+
PATTERNS_TO_KEEP = [
|
|
36
|
+
r"kitty-specs/\*\*/tasks/", # kitty-specs/**/tasks/*.md
|
|
37
|
+
r"kitty-specs/.*/tasks/", # kitty-specs/*/tasks/*.md
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_blocking_pattern(line: str) -> bool:
|
|
42
|
+
"""Check if a line blocks the entire kitty-specs directory.
|
|
43
|
+
|
|
44
|
+
Returns True for patterns like:
|
|
45
|
+
- kitty-specs
|
|
46
|
+
- kitty-specs/
|
|
47
|
+
- /kitty-specs
|
|
48
|
+
- /kitty-specs/
|
|
49
|
+
|
|
50
|
+
Returns False for specific subpath patterns like:
|
|
51
|
+
- kitty-specs/**/tasks/*.md (this is fine, used in worktrees)
|
|
52
|
+
"""
|
|
53
|
+
stripped = line.strip()
|
|
54
|
+
|
|
55
|
+
# Skip comments and empty lines
|
|
56
|
+
if not stripped or stripped.startswith("#"):
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Check if it's a specific subpath pattern (KEEP these)
|
|
60
|
+
for keep_pattern in PATTERNS_TO_KEEP:
|
|
61
|
+
if re.search(keep_pattern, stripped):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# Check if it blocks the entire directory (REMOVE these)
|
|
65
|
+
for remove_pattern in PATTERNS_TO_REMOVE:
|
|
66
|
+
if re.match(remove_pattern, stripped):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find_blocking_entries(gitignore_path: Path) -> List[Tuple[int, str]]:
|
|
73
|
+
"""Find all lines that block kitty-specs/ entirely.
|
|
74
|
+
|
|
75
|
+
Returns list of (line_number, line_content) tuples.
|
|
76
|
+
Line numbers are 1-indexed for user display.
|
|
77
|
+
"""
|
|
78
|
+
if not gitignore_path.exists():
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
blocking_entries = []
|
|
82
|
+
content = gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
|
|
83
|
+
|
|
84
|
+
for i, line in enumerate(content.splitlines(), start=1):
|
|
85
|
+
if is_blocking_pattern(line):
|
|
86
|
+
blocking_entries.append((i, line.strip()))
|
|
87
|
+
|
|
88
|
+
return blocking_entries
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def remove_blocking_entries(gitignore_path: Path, dry_run: bool = False) -> Tuple[List[str], List[str]]:
|
|
92
|
+
"""Remove entries that block kitty-specs/ from .gitignore.
|
|
93
|
+
|
|
94
|
+
Returns (changes, errors) tuple.
|
|
95
|
+
"""
|
|
96
|
+
changes: List[str] = []
|
|
97
|
+
errors: List[str] = []
|
|
98
|
+
|
|
99
|
+
if not gitignore_path.exists():
|
|
100
|
+
changes.append("No .gitignore file found")
|
|
101
|
+
return changes, errors
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
content = gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
|
|
105
|
+
except OSError as e:
|
|
106
|
+
errors.append(f"Failed to read .gitignore: {e}")
|
|
107
|
+
return changes, errors
|
|
108
|
+
|
|
109
|
+
lines = content.splitlines(keepends=True)
|
|
110
|
+
new_lines = []
|
|
111
|
+
removed_count = 0
|
|
112
|
+
|
|
113
|
+
for i, line in enumerate(lines, start=1):
|
|
114
|
+
if is_blocking_pattern(line):
|
|
115
|
+
changes.append(f"Line {i}: Removed '{line.strip()}'")
|
|
116
|
+
removed_count += 1
|
|
117
|
+
# Skip this line (don't add to new_lines)
|
|
118
|
+
else:
|
|
119
|
+
new_lines.append(line)
|
|
120
|
+
|
|
121
|
+
if removed_count == 0:
|
|
122
|
+
changes.append("No blocking kitty-specs/ entries found in .gitignore")
|
|
123
|
+
return changes, errors
|
|
124
|
+
|
|
125
|
+
if dry_run:
|
|
126
|
+
changes.insert(0, f"Would remove {removed_count} blocking entries from .gitignore")
|
|
127
|
+
return changes, errors
|
|
128
|
+
|
|
129
|
+
# Write updated content
|
|
130
|
+
try:
|
|
131
|
+
new_content = "".join(new_lines)
|
|
132
|
+
# Clean up any resulting double blank lines
|
|
133
|
+
new_content = re.sub(r"\n{3,}", "\n\n", new_content)
|
|
134
|
+
gitignore_path.write_text(new_content, encoding="utf-8")
|
|
135
|
+
changes.insert(0, f"Removed {removed_count} blocking entries from .gitignore")
|
|
136
|
+
except OSError as e:
|
|
137
|
+
errors.append(f"Failed to write .gitignore: {e}")
|
|
138
|
+
|
|
139
|
+
return changes, errors
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@MigrationRegistry.register
|
|
143
|
+
class RemoveKittySpecsFromGitignoreMigration(BaseMigration):
|
|
144
|
+
"""Remove kitty-specs/ from main repo .gitignore.
|
|
145
|
+
|
|
146
|
+
Feature specifications must be tracked in git. If kitty-specs/ is in
|
|
147
|
+
.gitignore, git add operations fail during feature creation.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
migration_id = MIGRATION_ID
|
|
151
|
+
description = MIGRATION_DESCRIPTION
|
|
152
|
+
target_version = MIGRATION_VERSION
|
|
153
|
+
|
|
154
|
+
def detect(self, project_path: Path) -> bool:
|
|
155
|
+
"""Check if .gitignore contains blocking kitty-specs/ entries."""
|
|
156
|
+
gitignore_path = project_path / ".gitignore"
|
|
157
|
+
blocking_entries = find_blocking_entries(gitignore_path)
|
|
158
|
+
return len(blocking_entries) > 0
|
|
159
|
+
|
|
160
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
161
|
+
"""Check if we can modify .gitignore."""
|
|
162
|
+
gitignore_path = project_path / ".gitignore"
|
|
163
|
+
|
|
164
|
+
if not gitignore_path.exists():
|
|
165
|
+
return True, ""
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
gitignore_path.read_text(encoding="utf-8-sig", errors="ignore")
|
|
169
|
+
return True, ""
|
|
170
|
+
except OSError as e:
|
|
171
|
+
return False, f".gitignore is not readable: {e}"
|
|
172
|
+
|
|
173
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
174
|
+
"""Remove blocking kitty-specs/ entries from .gitignore."""
|
|
175
|
+
gitignore_path = project_path / ".gitignore"
|
|
176
|
+
changes, errors = remove_blocking_entries(gitignore_path, dry_run=dry_run)
|
|
177
|
+
|
|
178
|
+
return MigrationResult(
|
|
179
|
+
success=len(errors) == 0,
|
|
180
|
+
changes_made=changes,
|
|
181
|
+
errors=errors,
|
|
182
|
+
warnings=[],
|
|
183
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Migration: Rename .specify/ to .kittify/ and /specs/ to /kitty-specs/."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..registry import MigrationRegistry
|
|
9
|
+
from .base import BaseMigration, MigrationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@MigrationRegistry.register
|
|
13
|
+
class SpecifyToKittifyMigration(BaseMigration):
|
|
14
|
+
"""Migrate from .specify/ to .kittify/ and /specs/ to /kitty-specs/.
|
|
15
|
+
|
|
16
|
+
This migration handles the rebranding from the original "specify"
|
|
17
|
+
naming to "kittify" naming introduced in v0.2.0.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
migration_id = "0.2.0_specify_to_kittify"
|
|
21
|
+
description = "Rename .specify/ to .kittify/ and /specs/ to /kitty-specs/"
|
|
22
|
+
target_version = "0.2.0"
|
|
23
|
+
|
|
24
|
+
def detect(self, project_path: Path) -> bool:
|
|
25
|
+
"""Check if project uses old .specify/ directory."""
|
|
26
|
+
return (project_path / ".specify").exists()
|
|
27
|
+
|
|
28
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
29
|
+
"""Check if migration can be safely applied."""
|
|
30
|
+
specify_dir = project_path / ".specify"
|
|
31
|
+
kittify_dir = project_path / ".kittify"
|
|
32
|
+
|
|
33
|
+
if not specify_dir.exists():
|
|
34
|
+
return False, ".specify/ directory does not exist"
|
|
35
|
+
|
|
36
|
+
if kittify_dir.exists():
|
|
37
|
+
return False, ".kittify/ already exists - manual merge required"
|
|
38
|
+
|
|
39
|
+
specs_dir = project_path / "specs"
|
|
40
|
+
kitty_specs_dir = project_path / "kitty-specs"
|
|
41
|
+
|
|
42
|
+
if specs_dir.exists() and kitty_specs_dir.exists():
|
|
43
|
+
return False, "Both /specs/ and /kitty-specs/ exist - manual merge required"
|
|
44
|
+
|
|
45
|
+
return True, ""
|
|
46
|
+
|
|
47
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
48
|
+
"""Apply the migration."""
|
|
49
|
+
changes: list[str] = []
|
|
50
|
+
errors: list[str] = []
|
|
51
|
+
|
|
52
|
+
specify_dir = project_path / ".specify"
|
|
53
|
+
kittify_dir = project_path / ".kittify"
|
|
54
|
+
specs_dir = project_path / "specs"
|
|
55
|
+
kitty_specs_dir = project_path / "kitty-specs"
|
|
56
|
+
|
|
57
|
+
# Rename .specify/ to .kittify/
|
|
58
|
+
if specify_dir.exists():
|
|
59
|
+
if dry_run:
|
|
60
|
+
changes.append(f"Would rename {specify_dir} to {kittify_dir}")
|
|
61
|
+
else:
|
|
62
|
+
try:
|
|
63
|
+
shutil.move(str(specify_dir), str(kittify_dir))
|
|
64
|
+
changes.append(f"Renamed {specify_dir} to {kittify_dir}")
|
|
65
|
+
except OSError as e:
|
|
66
|
+
errors.append(f"Failed to rename .specify/ to .kittify/: {e}")
|
|
67
|
+
|
|
68
|
+
# Rename /specs/ to /kitty-specs/
|
|
69
|
+
if specs_dir.exists() and not kitty_specs_dir.exists():
|
|
70
|
+
if dry_run:
|
|
71
|
+
changes.append(f"Would rename {specs_dir} to {kitty_specs_dir}")
|
|
72
|
+
else:
|
|
73
|
+
try:
|
|
74
|
+
shutil.move(str(specs_dir), str(kitty_specs_dir))
|
|
75
|
+
changes.append(f"Renamed {specs_dir} to {kitty_specs_dir}")
|
|
76
|
+
except OSError as e:
|
|
77
|
+
errors.append(f"Failed to rename /specs/ to /kitty-specs/: {e}")
|
|
78
|
+
|
|
79
|
+
success = len(errors) == 0
|
|
80
|
+
return MigrationResult(success=success, changes_made=changes, errors=errors)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Migration: Ensure all 12 agent directories are in .gitignore."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..registry import MigrationRegistry
|
|
8
|
+
from .base import BaseMigration, MigrationResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@MigrationRegistry.register
|
|
12
|
+
class GitignoreAgentsMigration(BaseMigration):
|
|
13
|
+
"""Ensure all 12 agent directories are in .gitignore.
|
|
14
|
+
|
|
15
|
+
This migration adds protection for all known AI agent directories
|
|
16
|
+
that should never be committed to git (they contain auth tokens).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
migration_id = "0.4.8_gitignore_agents"
|
|
20
|
+
description = "Add all 12 AI agent directories to .gitignore"
|
|
21
|
+
target_version = "0.4.8"
|
|
22
|
+
|
|
23
|
+
EXPECTED_AGENTS = [
|
|
24
|
+
".claude/",
|
|
25
|
+
".codex/",
|
|
26
|
+
".opencode/",
|
|
27
|
+
".windsurf/",
|
|
28
|
+
".gemini/",
|
|
29
|
+
".cursor/",
|
|
30
|
+
".qwen/",
|
|
31
|
+
".kilocode/",
|
|
32
|
+
".augment/",
|
|
33
|
+
".roo/",
|
|
34
|
+
".amazonq/",
|
|
35
|
+
".github/copilot/",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def detect(self, project_path: Path) -> bool:
|
|
39
|
+
"""Check if .gitignore is missing agent directories."""
|
|
40
|
+
gitignore = project_path / ".gitignore"
|
|
41
|
+
if not gitignore.exists():
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
|
|
46
|
+
except OSError:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
missing = [d for d in self.EXPECTED_AGENTS if d not in content]
|
|
50
|
+
return len(missing) > 0
|
|
51
|
+
|
|
52
|
+
def can_apply(self, project_path: Path) -> tuple[bool, str]:
|
|
53
|
+
"""Check if we can modify .gitignore."""
|
|
54
|
+
gitignore = project_path / ".gitignore"
|
|
55
|
+
|
|
56
|
+
if gitignore.exists():
|
|
57
|
+
try:
|
|
58
|
+
# Test read access; tolerate BOM and ignore invalid UTF-8 bytes
|
|
59
|
+
gitignore.read_text(encoding="utf-8-sig", errors="ignore")
|
|
60
|
+
except (OSError, UnicodeDecodeError):
|
|
61
|
+
return False, ".gitignore is not readable"
|
|
62
|
+
|
|
63
|
+
return True, ""
|
|
64
|
+
|
|
65
|
+
def apply(self, project_path: Path, dry_run: bool = False) -> MigrationResult:
|
|
66
|
+
"""Apply gitignore updates."""
|
|
67
|
+
changes: list[str] = []
|
|
68
|
+
warnings: list[str] = []
|
|
69
|
+
errors: list[str] = []
|
|
70
|
+
|
|
71
|
+
gitignore = project_path / ".gitignore"
|
|
72
|
+
|
|
73
|
+
# Determine what needs to be added
|
|
74
|
+
existing_content = ""
|
|
75
|
+
if gitignore.exists():
|
|
76
|
+
try:
|
|
77
|
+
existing_content = gitignore.read_text(encoding="utf-8-sig", errors="ignore")
|
|
78
|
+
except OSError as e:
|
|
79
|
+
errors.append(f"Failed to read .gitignore: {e}")
|
|
80
|
+
return MigrationResult(success=False, errors=errors)
|
|
81
|
+
|
|
82
|
+
missing = [d for d in self.EXPECTED_AGENTS if d not in existing_content]
|
|
83
|
+
|
|
84
|
+
if not missing:
|
|
85
|
+
changes.append("All agent directories already in .gitignore")
|
|
86
|
+
return MigrationResult(success=True, changes_made=changes)
|
|
87
|
+
|
|
88
|
+
if dry_run:
|
|
89
|
+
changes.append(f"Would add {len(missing)} agent directories to .gitignore")
|
|
90
|
+
for d in missing:
|
|
91
|
+
changes.append(f" - {d}")
|
|
92
|
+
return MigrationResult(success=True, changes_made=changes)
|
|
93
|
+
|
|
94
|
+
# Build new content
|
|
95
|
+
new_entries = "\n# AI Agent directories (added by Spec Kitty CLI)\n"
|
|
96
|
+
new_entries += "# These contain auth tokens and should NEVER be committed\n"
|
|
97
|
+
for d in missing:
|
|
98
|
+
new_entries += f"{d}\n"
|
|
99
|
+
|
|
100
|
+
# Ensure existing content ends with newline
|
|
101
|
+
if existing_content and not existing_content.endswith("\n"):
|
|
102
|
+
existing_content += "\n"
|
|
103
|
+
|
|
104
|
+
new_content = existing_content + new_entries
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
gitignore.write_text(new_content, encoding="utf-8")
|
|
108
|
+
changes.append(f"Added {len(missing)} agent directories to .gitignore")
|
|
109
|
+
except OSError as e:
|
|
110
|
+
errors.append(f"Failed to write .gitignore: {e}")
|
|
111
|
+
|
|
112
|
+
success = len(errors) == 0
|
|
113
|
+
return MigrationResult(
|
|
114
|
+
success=success,
|
|
115
|
+
changes_made=changes,
|
|
116
|
+
errors=errors,
|
|
117
|
+
warnings=warnings,
|
|
118
|
+
)
|