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,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitignoreManager module for protecting AI agent directories.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized system for managing .gitignore entries
|
|
5
|
+
to protect AI agent directories from being accidentally committed to git.
|
|
6
|
+
It replaces the fragmented approach where only .codex/ was protected.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional, Set
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class AgentDirectory:
|
|
17
|
+
"""Represents a single agent's directory that needs protection."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
"""Agent name identifier (e.g., 'claude', 'codex')"""
|
|
21
|
+
|
|
22
|
+
directory: str
|
|
23
|
+
"""Directory path with trailing slash (e.g., '.claude/')"""
|
|
24
|
+
|
|
25
|
+
is_special: bool
|
|
26
|
+
"""Indicates if special handling is needed (e.g., .github/)"""
|
|
27
|
+
|
|
28
|
+
description: str
|
|
29
|
+
"""Human-readable description for documentation"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ProtectionResult:
|
|
34
|
+
"""Result of a gitignore protection operation."""
|
|
35
|
+
|
|
36
|
+
success: bool
|
|
37
|
+
"""Whether the operation succeeded"""
|
|
38
|
+
|
|
39
|
+
modified: bool
|
|
40
|
+
"""Whether .gitignore was modified"""
|
|
41
|
+
|
|
42
|
+
entries_added: List[str] = field(default_factory=list)
|
|
43
|
+
"""New entries added to .gitignore"""
|
|
44
|
+
|
|
45
|
+
entries_skipped: List[str] = field(default_factory=list)
|
|
46
|
+
"""Entries already present in .gitignore"""
|
|
47
|
+
|
|
48
|
+
errors: List[str] = field(default_factory=list)
|
|
49
|
+
"""Error messages if any occurred"""
|
|
50
|
+
|
|
51
|
+
warnings: List[str] = field(default_factory=list)
|
|
52
|
+
"""Warning messages if any were generated"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Registry of all known AI agent directories
|
|
56
|
+
AGENT_DIRECTORIES = [
|
|
57
|
+
AgentDirectory("claude", ".claude/", False, "Claude Code CLI"),
|
|
58
|
+
AgentDirectory("codex", ".codex/", False, "Codex (contains auth.json)"),
|
|
59
|
+
AgentDirectory("opencode", ".opencode/", False, "opencode CLI"),
|
|
60
|
+
AgentDirectory("windsurf", ".windsurf/", False, "Windsurf"),
|
|
61
|
+
AgentDirectory("gemini", ".gemini/", False, "Google Gemini"),
|
|
62
|
+
AgentDirectory("cursor", ".cursor/", False, "Cursor"),
|
|
63
|
+
AgentDirectory("qwen", ".qwen/", False, "Qwen"),
|
|
64
|
+
AgentDirectory("kilocode", ".kilocode/", False, "Kilocode"),
|
|
65
|
+
AgentDirectory("auggie", ".augment/", False, "Auggie"),
|
|
66
|
+
AgentDirectory("roo", ".roo/", False, "Roo Coder"),
|
|
67
|
+
AgentDirectory("amazonq", ".amazonq/", False, "Amazon Q"),
|
|
68
|
+
AgentDirectory("copilot", ".github/copilot/", True, "GitHub Copilot (user settings)"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GitignoreManager:
|
|
73
|
+
"""Manages gitignore entries for AI agent directories."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, project_path: Path):
|
|
76
|
+
"""
|
|
77
|
+
Initialize GitignoreManager with project root path.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
project_path: Root directory of the project
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If project_path doesn't exist or isn't a directory
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(project_path, Path):
|
|
86
|
+
project_path = Path(project_path)
|
|
87
|
+
|
|
88
|
+
if not project_path.exists():
|
|
89
|
+
raise ValueError(f"Project path does not exist: {project_path}")
|
|
90
|
+
|
|
91
|
+
if not project_path.is_dir():
|
|
92
|
+
raise ValueError(f"Project path is not a directory: {project_path}")
|
|
93
|
+
|
|
94
|
+
self.project_path = project_path
|
|
95
|
+
self.gitignore_path = project_path / ".gitignore"
|
|
96
|
+
self.marker = "# Added by Spec Kitty CLI (auto-managed)"
|
|
97
|
+
self._line_ending = None
|
|
98
|
+
|
|
99
|
+
def ensure_entries(self, entries: List[str]) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Core method to add entries to .gitignore.
|
|
102
|
+
|
|
103
|
+
This method migrates the logic from the original ensure_gitignore_entries
|
|
104
|
+
function, maintaining the same behavior for compatibility.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
entries: List of gitignore patterns to add
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if .gitignore was modified, False otherwise
|
|
111
|
+
"""
|
|
112
|
+
if not entries:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Read existing content or start with empty list
|
|
116
|
+
if self.gitignore_path.exists():
|
|
117
|
+
content = self.gitignore_path.read_text(encoding="utf-8-sig")
|
|
118
|
+
# Detect and store line ending style
|
|
119
|
+
self._line_ending = self._detect_line_ending(content)
|
|
120
|
+
lines = content.splitlines()
|
|
121
|
+
else:
|
|
122
|
+
lines = []
|
|
123
|
+
# Use system default for new files
|
|
124
|
+
self._line_ending = os.linesep
|
|
125
|
+
|
|
126
|
+
existing = set(lines)
|
|
127
|
+
changed = False
|
|
128
|
+
|
|
129
|
+
# Check if any entry needs to be added
|
|
130
|
+
if any(entry not in existing for entry in entries):
|
|
131
|
+
# Add marker if not present
|
|
132
|
+
if self.marker not in existing:
|
|
133
|
+
if lines and lines[-1].strip():
|
|
134
|
+
lines.append("") # Add blank line before marker
|
|
135
|
+
lines.append(self.marker)
|
|
136
|
+
existing.add(self.marker)
|
|
137
|
+
changed = True
|
|
138
|
+
|
|
139
|
+
# Add missing entries
|
|
140
|
+
for entry in entries:
|
|
141
|
+
if entry not in existing:
|
|
142
|
+
lines.append(entry)
|
|
143
|
+
existing.add(entry)
|
|
144
|
+
changed = True
|
|
145
|
+
|
|
146
|
+
# Write back if changed
|
|
147
|
+
if changed:
|
|
148
|
+
# Ensure file ends with newline
|
|
149
|
+
if lines and lines[-1] != "":
|
|
150
|
+
lines.append("")
|
|
151
|
+
|
|
152
|
+
# Join with detected line ending
|
|
153
|
+
content = self._line_ending.join(lines)
|
|
154
|
+
self.gitignore_path.write_text(content, encoding="utf-8")
|
|
155
|
+
|
|
156
|
+
return changed
|
|
157
|
+
|
|
158
|
+
def _detect_line_ending(self, content: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Detect and return the line ending style used in content.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
content: File content to analyze
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Line ending string ('\r\n' for Windows, '\n' for Unix/Mac)
|
|
167
|
+
"""
|
|
168
|
+
if '\r\n' in content:
|
|
169
|
+
return '\r\n'
|
|
170
|
+
else:
|
|
171
|
+
return '\n'
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def get_agent_directories(cls) -> List[AgentDirectory]:
|
|
175
|
+
"""
|
|
176
|
+
Get a copy of the registry of all known agent directories.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of AgentDirectory objects representing all known agents
|
|
180
|
+
"""
|
|
181
|
+
# Return a copy to prevent external modification
|
|
182
|
+
return AGENT_DIRECTORIES.copy()
|
|
183
|
+
|
|
184
|
+
def protect_all_agents(self) -> ProtectionResult:
|
|
185
|
+
"""
|
|
186
|
+
Add all known agent directories to .gitignore.
|
|
187
|
+
|
|
188
|
+
This is the primary method used during spec-kitty init to ensure
|
|
189
|
+
comprehensive protection of all AI agent directories.
|
|
190
|
+
|
|
191
|
+
Also protects runtime files like .kittify/.dashboard.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
ProtectionResult containing details of the operation
|
|
195
|
+
"""
|
|
196
|
+
result = ProtectionResult(success=True, modified=False)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Get all agent directories
|
|
200
|
+
all_directories = [agent.directory for agent in AGENT_DIRECTORIES]
|
|
201
|
+
|
|
202
|
+
# Add runtime files that should never be tracked
|
|
203
|
+
all_directories.append(".kittify/.dashboard")
|
|
204
|
+
|
|
205
|
+
# Track existing entries before modification
|
|
206
|
+
existing_before = set()
|
|
207
|
+
if self.gitignore_path.exists():
|
|
208
|
+
content = self.gitignore_path.read_text(encoding="utf-8-sig")
|
|
209
|
+
existing_before = set(content.splitlines())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Attempt to add all directories
|
|
213
|
+
modified = self.ensure_entries(all_directories)
|
|
214
|
+
result.modified = modified
|
|
215
|
+
|
|
216
|
+
# Track what was added vs skipped
|
|
217
|
+
if self.gitignore_path.exists():
|
|
218
|
+
content = self.gitignore_path.read_text(encoding="utf-8-sig")
|
|
219
|
+
existing_after = set(content.splitlines())
|
|
220
|
+
|
|
221
|
+
for directory in all_directories:
|
|
222
|
+
if directory in existing_after:
|
|
223
|
+
if directory not in existing_before:
|
|
224
|
+
result.entries_added.append(directory)
|
|
225
|
+
else:
|
|
226
|
+
result.entries_skipped.append(directory)
|
|
227
|
+
|
|
228
|
+
except PermissionError as e:
|
|
229
|
+
result.success = False
|
|
230
|
+
result.errors.append(
|
|
231
|
+
f"Cannot update .gitignore: Permission denied. Run: chmod u+w {self.gitignore_path}"
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
result.success = False
|
|
235
|
+
result.errors.append(f"Error protecting agent directories: {str(e)}")
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
def protect_selected_agents(self, agents: List[str]) -> ProtectionResult:
|
|
240
|
+
"""
|
|
241
|
+
Add specific agent directories to .gitignore based on selection.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
agents: List of agent names (e.g., ['claude', 'codex', 'opencode'])
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
ProtectionResult containing details of the operation
|
|
248
|
+
"""
|
|
249
|
+
result = ProtectionResult(success=True, modified=False)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# Build mapping of agent names to directories
|
|
253
|
+
agent_map = {agent.name: agent for agent in AGENT_DIRECTORIES}
|
|
254
|
+
|
|
255
|
+
# Collect directories for selected agents
|
|
256
|
+
directories_to_add = []
|
|
257
|
+
for agent_name in agents:
|
|
258
|
+
if agent_name in agent_map:
|
|
259
|
+
agent = agent_map[agent_name]
|
|
260
|
+
directories_to_add.append(agent.directory)
|
|
261
|
+
|
|
262
|
+
else:
|
|
263
|
+
result.warnings.append(f"Unknown agent name: {agent_name}")
|
|
264
|
+
|
|
265
|
+
if not directories_to_add:
|
|
266
|
+
result.warnings.append("No valid agent directories to add")
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
# Track existing entries before modification
|
|
270
|
+
existing_before = set()
|
|
271
|
+
if self.gitignore_path.exists():
|
|
272
|
+
content = self.gitignore_path.read_text(encoding="utf-8-sig")
|
|
273
|
+
existing_before = set(content.splitlines())
|
|
274
|
+
|
|
275
|
+
# Attempt to add selected directories
|
|
276
|
+
modified = self.ensure_entries(directories_to_add)
|
|
277
|
+
result.modified = modified
|
|
278
|
+
|
|
279
|
+
# Track what was added vs skipped
|
|
280
|
+
if self.gitignore_path.exists():
|
|
281
|
+
content = self.gitignore_path.read_text(encoding="utf-8-sig")
|
|
282
|
+
existing_after = set(content.splitlines())
|
|
283
|
+
|
|
284
|
+
for directory in directories_to_add:
|
|
285
|
+
if directory in existing_after:
|
|
286
|
+
if directory not in existing_before:
|
|
287
|
+
result.entries_added.append(directory)
|
|
288
|
+
else:
|
|
289
|
+
result.entries_skipped.append(directory)
|
|
290
|
+
|
|
291
|
+
except PermissionError as e:
|
|
292
|
+
result.success = False
|
|
293
|
+
result.errors.append(
|
|
294
|
+
f"Cannot update .gitignore: Permission denied. Run: chmod u+w {self.gitignore_path}"
|
|
295
|
+
)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
result.success = False
|
|
298
|
+
result.errors.append(f"Error protecting selected agents: {str(e)}")
|
|
299
|
+
|
|
300
|
+
return result
|
specify_cli/guards.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Shared pre-flight validation utilities for Spec Kitty CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GuardValidationError(Exception):
|
|
13
|
+
"""Raised when pre-flight validation fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class WorktreeValidationResult:
|
|
18
|
+
"""Result of worktree location validation."""
|
|
19
|
+
|
|
20
|
+
current_branch: str
|
|
21
|
+
is_feature_branch: bool
|
|
22
|
+
is_main_branch: bool
|
|
23
|
+
worktree_path: Optional[Path]
|
|
24
|
+
errors: List[str]
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_valid(self) -> bool:
|
|
28
|
+
"""Return True when validation passed."""
|
|
29
|
+
if self.errors:
|
|
30
|
+
return False
|
|
31
|
+
if not self.current_branch:
|
|
32
|
+
return True
|
|
33
|
+
return self.is_feature_branch and not self.is_main_branch
|
|
34
|
+
|
|
35
|
+
def format_error(self) -> str:
|
|
36
|
+
"""Format error message for display."""
|
|
37
|
+
if not self.errors:
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
output = ["Location Pre-flight Check Failed:", ""]
|
|
41
|
+
for error in self.errors:
|
|
42
|
+
output.append(f" {error}")
|
|
43
|
+
|
|
44
|
+
if self.is_main_branch:
|
|
45
|
+
output.extend(
|
|
46
|
+
[
|
|
47
|
+
"",
|
|
48
|
+
"You are on the 'main' branch. Commands must run from feature worktrees.",
|
|
49
|
+
"",
|
|
50
|
+
"Available worktrees:",
|
|
51
|
+
" $ ls .worktrees/",
|
|
52
|
+
"",
|
|
53
|
+
"Navigate to worktree:",
|
|
54
|
+
" $ cd .worktrees/<feature-name>",
|
|
55
|
+
"",
|
|
56
|
+
"Verify branch:",
|
|
57
|
+
" $ git branch --show-current",
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return "\n".join(output)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_worktree_location(project_root: Optional[Path] = None) -> WorktreeValidationResult:
|
|
65
|
+
"""Validate that commands run from a feature worktree."""
|
|
66
|
+
project_root = Path(project_root) if project_root is not None else Path.cwd()
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
["git", "branch", "--show-current"],
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
cwd=project_root,
|
|
74
|
+
check=False,
|
|
75
|
+
)
|
|
76
|
+
except FileNotFoundError as exc:
|
|
77
|
+
raise GuardValidationError("git executable not found") from exc
|
|
78
|
+
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
return WorktreeValidationResult(
|
|
81
|
+
current_branch="unknown",
|
|
82
|
+
is_feature_branch=False,
|
|
83
|
+
is_main_branch=False,
|
|
84
|
+
worktree_path=None,
|
|
85
|
+
errors=["Not a git repository"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
current_branch = result.stdout.strip()
|
|
89
|
+
is_main_branch = current_branch in {"main", "master"}
|
|
90
|
+
is_feature_branch = bool(re.match(r"^\d{3}-[\w-]+$", current_branch))
|
|
91
|
+
|
|
92
|
+
errors: List[str] = []
|
|
93
|
+
if not current_branch:
|
|
94
|
+
errors.append("Unable to determine current git branch.")
|
|
95
|
+
elif is_main_branch:
|
|
96
|
+
errors.append("Command must run from feature worktree, not main branch.")
|
|
97
|
+
elif not is_feature_branch:
|
|
98
|
+
errors.append(
|
|
99
|
+
f"Unexpected branch '{current_branch}'. Commands must run from feature worktrees."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
worktree_path = project_root if is_feature_branch and not errors else None
|
|
103
|
+
|
|
104
|
+
return WorktreeValidationResult(
|
|
105
|
+
current_branch=current_branch or "unknown",
|
|
106
|
+
is_feature_branch=is_feature_branch,
|
|
107
|
+
is_main_branch=is_main_branch,
|
|
108
|
+
worktree_path=worktree_path,
|
|
109
|
+
errors=errors,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_git_clean(project_root: Optional[Path] = None) -> WorktreeValidationResult:
|
|
114
|
+
"""Validate git repository has no uncommitted changes."""
|
|
115
|
+
project_root = Path(project_root) if project_root is not None else Path.cwd()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
["git", "status", "--porcelain"],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
text=True,
|
|
122
|
+
cwd=project_root,
|
|
123
|
+
check=False,
|
|
124
|
+
)
|
|
125
|
+
except FileNotFoundError as exc:
|
|
126
|
+
raise GuardValidationError("git executable not found") from exc
|
|
127
|
+
|
|
128
|
+
errors: List[str] = []
|
|
129
|
+
if result.returncode != 0:
|
|
130
|
+
errors.append("Unable to read git status.")
|
|
131
|
+
else:
|
|
132
|
+
status_lines = [line for line in result.stdout.splitlines() if line.strip()]
|
|
133
|
+
if status_lines:
|
|
134
|
+
errors.append(
|
|
135
|
+
f"Uncommitted changes detected ({len(status_lines)} files). "
|
|
136
|
+
"Commit or stash changes before switching missions."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return WorktreeValidationResult(
|
|
140
|
+
current_branch="",
|
|
141
|
+
is_feature_branch=not errors,
|
|
142
|
+
is_main_branch=False,
|
|
143
|
+
worktree_path=None,
|
|
144
|
+
errors=errors,
|
|
145
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Legacy format detection for Spec Kitty lane management.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to detect whether a feature uses the old
|
|
5
|
+
directory-based lane structure (tasks/planned/, tasks/doing/, etc.) or
|
|
6
|
+
the new frontmatter-only lane system (flat tasks/ directory).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
# Lane directories that indicate legacy format when they contain .md files
|
|
15
|
+
LEGACY_LANE_DIRS: List[str] = ["planned", "doing", "for_review", "done"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_legacy_format(feature_path: Path) -> bool:
|
|
19
|
+
"""Check if feature uses legacy directory-based lanes.
|
|
20
|
+
|
|
21
|
+
A feature is considered to use legacy format if:
|
|
22
|
+
- It has a tasks/ subdirectory
|
|
23
|
+
- Any of the lane subdirectories (planned/, doing/, for_review/, done/)
|
|
24
|
+
exist AND contain at least one .md file
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
feature_path: Path to the feature directory (e.g., kitty-specs/007-feature/)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if legacy directory-based lanes detected, False otherwise.
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
Empty lane directories (containing only .gitkeep) are NOT considered
|
|
34
|
+
legacy format - only directories with actual .md work package files.
|
|
35
|
+
"""
|
|
36
|
+
tasks_dir = feature_path / "tasks"
|
|
37
|
+
if not tasks_dir.exists():
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
for lane in LEGACY_LANE_DIRS:
|
|
41
|
+
lane_path = tasks_dir / lane
|
|
42
|
+
if lane_path.is_dir():
|
|
43
|
+
# Check if there are any .md files (not just .gitkeep)
|
|
44
|
+
md_files = list(lane_path.glob("*.md"))
|
|
45
|
+
if md_files:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_legacy_lane_counts(feature_path: Path) -> dict[str, int]:
|
|
52
|
+
"""Get count of work packages in each legacy lane directory.
|
|
53
|
+
|
|
54
|
+
Useful for migration reporting and validation.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
feature_path: Path to the feature directory
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dictionary mapping lane names to count of .md files in each.
|
|
61
|
+
Only includes lanes that have files.
|
|
62
|
+
"""
|
|
63
|
+
tasks_dir = feature_path / "tasks"
|
|
64
|
+
counts: dict[str, int] = {}
|
|
65
|
+
|
|
66
|
+
if not tasks_dir.exists():
|
|
67
|
+
return counts
|
|
68
|
+
|
|
69
|
+
for lane in LEGACY_LANE_DIRS:
|
|
70
|
+
lane_path = tasks_dir / lane
|
|
71
|
+
if lane_path.is_dir():
|
|
72
|
+
md_files = list(lane_path.glob("*.md"))
|
|
73
|
+
if md_files:
|
|
74
|
+
counts[lane] = len(md_files)
|
|
75
|
+
|
|
76
|
+
return counts
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"LEGACY_LANE_DIRS",
|
|
81
|
+
"is_legacy_format",
|
|
82
|
+
"get_legacy_lane_counts",
|
|
83
|
+
]
|