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,506 @@
|
|
|
1
|
+
"""Worktree management utilities for spec-kitty feature development.
|
|
2
|
+
|
|
3
|
+
This module provides functions for creating and managing workspaces (git worktrees
|
|
4
|
+
or jj workspaces) for parallel feature development. Uses the VCS abstraction layer
|
|
5
|
+
to support both git and jujutsu backends.
|
|
6
|
+
|
|
7
|
+
All functions are location-aware and work correctly whether called from main
|
|
8
|
+
repository or existing worktree/workspace.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import platform
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import warnings
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from .paths import locate_project_root
|
|
21
|
+
from .vcs import VCSBackend, get_vcs
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _exclude_from_git(worktree_path: Path, patterns: list[str]) -> None:
|
|
25
|
+
"""Add patterns to worktree's .git/info/exclude to prevent committing.
|
|
26
|
+
|
|
27
|
+
This prevents symlinks created in worktrees from being committed and
|
|
28
|
+
overwriting real files in main on merge (fixes issue #79).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
worktree_path: Path to the worktree root
|
|
32
|
+
patterns: List of patterns to exclude (e.g., [".kittify/memory"])
|
|
33
|
+
"""
|
|
34
|
+
# In a worktree, .git is a file pointing to the real git dir
|
|
35
|
+
git_path = worktree_path / ".git"
|
|
36
|
+
if not git_path.exists():
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Find the actual git directory
|
|
40
|
+
if git_path.is_file():
|
|
41
|
+
# Worktree: .git file contains "gitdir: /path/to/real/.git/worktrees/name"
|
|
42
|
+
try:
|
|
43
|
+
content = git_path.read_text().strip()
|
|
44
|
+
if content.startswith("gitdir:"):
|
|
45
|
+
git_dir = Path(content[7:].strip())
|
|
46
|
+
exclude_file = git_dir / "info" / "exclude"
|
|
47
|
+
else:
|
|
48
|
+
return
|
|
49
|
+
except (OSError, ValueError):
|
|
50
|
+
return
|
|
51
|
+
else:
|
|
52
|
+
# Regular repo or already resolved
|
|
53
|
+
exclude_file = git_path / "info" / "exclude"
|
|
54
|
+
|
|
55
|
+
# Ensure info directory exists
|
|
56
|
+
exclude_file.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Read existing exclusions
|
|
59
|
+
existing = set()
|
|
60
|
+
if exclude_file.exists():
|
|
61
|
+
try:
|
|
62
|
+
existing = set(exclude_file.read_text().splitlines())
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Add new patterns if not already present
|
|
67
|
+
new_patterns = [p for p in patterns if p not in existing]
|
|
68
|
+
if new_patterns:
|
|
69
|
+
try:
|
|
70
|
+
with exclude_file.open("a") as f:
|
|
71
|
+
# Add comment if this is our first addition
|
|
72
|
+
marker = "# Added by spec-kitty (worktree symlinks)"
|
|
73
|
+
if marker not in existing:
|
|
74
|
+
f.write(f"\n{marker}\n")
|
|
75
|
+
for pattern in new_patterns:
|
|
76
|
+
f.write(f"{pattern}\n")
|
|
77
|
+
except OSError:
|
|
78
|
+
# If we can't write, just skip - not critical
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_next_feature_number(repo_root: Path) -> int:
|
|
83
|
+
"""Determine next sequential feature number.
|
|
84
|
+
|
|
85
|
+
Scans both kitty-specs/ and .worktrees/ directories for existing features
|
|
86
|
+
(###-name format) and returns next number in sequence. This prevents number
|
|
87
|
+
reuse when features exist only in worktrees.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
repo_root: Repository root path
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Next feature number (e.g., 9 if highest existing is 008)
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
>>> repo_root = Path("/path/to/repo")
|
|
97
|
+
>>> next_num = get_next_feature_number(repo_root)
|
|
98
|
+
>>> assert next_num > 0
|
|
99
|
+
"""
|
|
100
|
+
max_number = 0
|
|
101
|
+
|
|
102
|
+
# Scan kitty-specs/ for feature numbers
|
|
103
|
+
specs_dir = repo_root / "kitty-specs"
|
|
104
|
+
if specs_dir.exists():
|
|
105
|
+
for item in specs_dir.iterdir():
|
|
106
|
+
if item.is_dir() and len(item.name) >= 3 and item.name[:3].isdigit():
|
|
107
|
+
try:
|
|
108
|
+
number = int(item.name[:3])
|
|
109
|
+
max_number = max(max_number, number)
|
|
110
|
+
except ValueError:
|
|
111
|
+
# Not a valid number, skip
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Also scan .worktrees/ for feature numbers
|
|
115
|
+
worktrees_dir = repo_root / ".worktrees"
|
|
116
|
+
if worktrees_dir.exists():
|
|
117
|
+
for item in worktrees_dir.iterdir():
|
|
118
|
+
if item.is_dir() and len(item.name) >= 3 and item.name[:3].isdigit():
|
|
119
|
+
try:
|
|
120
|
+
number = int(item.name[:3])
|
|
121
|
+
max_number = max(max_number, number)
|
|
122
|
+
except ValueError:
|
|
123
|
+
# Not a valid number, skip
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
return max_number + 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def create_feature_worktree(
|
|
130
|
+
repo_root: Path,
|
|
131
|
+
feature_slug: str,
|
|
132
|
+
feature_number: Optional[int] = None
|
|
133
|
+
) -> Tuple[Path, Path]:
|
|
134
|
+
"""Create workspace (git worktree or jj workspace) for feature development.
|
|
135
|
+
|
|
136
|
+
Creates a new workspace with a feature branch and sets up the
|
|
137
|
+
feature directory structure. Uses VCS abstraction to support both
|
|
138
|
+
git and jujutsu backends.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
repo_root: Repository root path
|
|
142
|
+
feature_slug: Feature identifier (e.g., "test-feature")
|
|
143
|
+
feature_number: Optional feature number (auto-detected if None)
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (worktree_path, feature_dir)
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
RuntimeError: If workspace creation fails
|
|
150
|
+
FileExistsError: If worktree path already exists
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
>>> repo_root = Path("/path/to/repo")
|
|
154
|
+
>>> worktree, feature_dir = create_feature_worktree(repo_root, "new-feature")
|
|
155
|
+
>>> assert worktree.exists()
|
|
156
|
+
>>> assert feature_dir.exists()
|
|
157
|
+
"""
|
|
158
|
+
# Auto-detect feature number if not provided
|
|
159
|
+
if feature_number is None:
|
|
160
|
+
feature_number = get_next_feature_number(repo_root)
|
|
161
|
+
|
|
162
|
+
# Format: 001-test-feature
|
|
163
|
+
branch_name = f"{feature_number:03d}-{feature_slug}"
|
|
164
|
+
|
|
165
|
+
# Create worktree at .worktrees/001-test-feature
|
|
166
|
+
worktree_path = repo_root / ".worktrees" / branch_name
|
|
167
|
+
|
|
168
|
+
# Ensure .worktrees directory exists
|
|
169
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
|
|
171
|
+
# Check if worktree already exists
|
|
172
|
+
if worktree_path.exists():
|
|
173
|
+
# Check if it's a valid workspace using VCS abstraction
|
|
174
|
+
is_valid_workspace = False
|
|
175
|
+
try:
|
|
176
|
+
vcs = get_vcs(worktree_path)
|
|
177
|
+
is_valid_workspace = vcs.is_repo(worktree_path)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# If VCS says no (or failed), fall back to simple git check
|
|
182
|
+
# A valid git worktree has .git as a file (pointing to main repo)
|
|
183
|
+
# or as a directory (standalone repo)
|
|
184
|
+
if not is_valid_workspace:
|
|
185
|
+
git_marker = worktree_path / ".git"
|
|
186
|
+
is_valid_workspace = git_marker.exists()
|
|
187
|
+
|
|
188
|
+
if is_valid_workspace:
|
|
189
|
+
feature_dir = worktree_path / "kitty-specs" / branch_name
|
|
190
|
+
return (worktree_path, feature_dir)
|
|
191
|
+
|
|
192
|
+
raise FileExistsError(f"Worktree path already exists: {worktree_path}")
|
|
193
|
+
|
|
194
|
+
# Get VCS implementation and create workspace
|
|
195
|
+
# NOTE: We do NOT use sparse_exclude for kitty-specs/ here because:
|
|
196
|
+
# - Users need to add research artifacts, patterns, etc. to kitty-specs/
|
|
197
|
+
# - The locate_work_package() function always uses main repo's kitty-specs/
|
|
198
|
+
# for WP operations, so stale copies in worktrees don't affect lane changes
|
|
199
|
+
try:
|
|
200
|
+
vcs = get_vcs(repo_root)
|
|
201
|
+
result = vcs.create_workspace(
|
|
202
|
+
workspace_path=worktree_path,
|
|
203
|
+
workspace_name=branch_name,
|
|
204
|
+
repo_root=repo_root,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not result.success:
|
|
208
|
+
raise RuntimeError(f"Failed to create workspace: {result.error}")
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
# If VCS abstraction fails, fall back to direct git command with warning
|
|
212
|
+
warnings.warn(
|
|
213
|
+
"VCS abstraction failed, falling back to direct git commands. "
|
|
214
|
+
"See: kitty-specs/015-first-class-jujutsu-vcs-integration/",
|
|
215
|
+
DeprecationWarning,
|
|
216
|
+
stacklevel=2,
|
|
217
|
+
)
|
|
218
|
+
try:
|
|
219
|
+
subprocess.run(
|
|
220
|
+
["git", "worktree", "add", str(worktree_path), "-b", branch_name],
|
|
221
|
+
cwd=repo_root,
|
|
222
|
+
check=True,
|
|
223
|
+
capture_output=True,
|
|
224
|
+
text=True
|
|
225
|
+
)
|
|
226
|
+
except subprocess.CalledProcessError as git_error:
|
|
227
|
+
raise RuntimeError(
|
|
228
|
+
f"Failed to create workspace: {git_error.stderr}"
|
|
229
|
+
) from git_error
|
|
230
|
+
|
|
231
|
+
# Create feature directory structure
|
|
232
|
+
feature_dir = worktree_path / "kitty-specs" / branch_name
|
|
233
|
+
feature_dir.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
|
|
235
|
+
# Setup feature directory (symlinks, subdirectories, etc.)
|
|
236
|
+
setup_feature_directory(feature_dir, worktree_path, repo_root)
|
|
237
|
+
|
|
238
|
+
return (worktree_path, feature_dir)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def setup_feature_directory(
|
|
242
|
+
feature_dir: Path,
|
|
243
|
+
worktree_path: Path,
|
|
244
|
+
repo_root: Path,
|
|
245
|
+
create_symlinks: bool = True
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Setup standard feature directory structure.
|
|
248
|
+
|
|
249
|
+
Creates:
|
|
250
|
+
- kitty-specs/###-name/ directory
|
|
251
|
+
- Subdirectories: checklists/, research/, tasks/
|
|
252
|
+
- Symlinks to .kittify/memory/ (or file copies on Windows)
|
|
253
|
+
- spec.md from template
|
|
254
|
+
- tasks/README.md
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
feature_dir: Feature directory path
|
|
258
|
+
worktree_path: Worktree root path
|
|
259
|
+
repo_root: Main repository root path
|
|
260
|
+
create_symlinks: If True, create symlinks; else copy files (Windows)
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
>>> feature_dir = Path("/path/to/.worktrees/001-feature/kitty-specs/001-feature")
|
|
264
|
+
>>> setup_feature_directory(feature_dir, feature_dir.parent.parent, repo_root)
|
|
265
|
+
>>> assert (feature_dir / "checklists").exists()
|
|
266
|
+
"""
|
|
267
|
+
# Ensure feature directory exists
|
|
268
|
+
feature_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
|
|
270
|
+
# Create subdirectories
|
|
271
|
+
(feature_dir / "checklists").mkdir(exist_ok=True)
|
|
272
|
+
(feature_dir / "research").mkdir(exist_ok=True)
|
|
273
|
+
tasks_dir = feature_dir / "tasks"
|
|
274
|
+
tasks_dir.mkdir(exist_ok=True)
|
|
275
|
+
|
|
276
|
+
# Create tasks/.gitkeep and README.md
|
|
277
|
+
(tasks_dir / ".gitkeep").touch()
|
|
278
|
+
|
|
279
|
+
# Create tasks/README.md with frontmatter format reference
|
|
280
|
+
tasks_readme_content = '''# Tasks Directory
|
|
281
|
+
|
|
282
|
+
This directory contains work package (WP) prompt files with lane status in frontmatter.
|
|
283
|
+
|
|
284
|
+
## Directory Structure (v0.9.0+)
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
tasks/
|
|
288
|
+
├── WP01-setup-infrastructure.md
|
|
289
|
+
├── WP02-user-authentication.md
|
|
290
|
+
├── WP03-api-endpoints.md
|
|
291
|
+
└── README.md
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
All WP files are stored flat in `tasks/`. The lane (planned, doing, for_review, done) is stored in the YAML frontmatter `lane:` field.
|
|
295
|
+
|
|
296
|
+
## Work Package File Format
|
|
297
|
+
|
|
298
|
+
Each WP file **MUST** use YAML frontmatter:
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
---
|
|
302
|
+
work_package_id: "WP01"
|
|
303
|
+
title: "Work Package Title"
|
|
304
|
+
lane: "planned"
|
|
305
|
+
subtasks:
|
|
306
|
+
- "T001"
|
|
307
|
+
- "T002"
|
|
308
|
+
phase: "Phase 1 - Setup"
|
|
309
|
+
assignee: ""
|
|
310
|
+
agent: ""
|
|
311
|
+
shell_pid: ""
|
|
312
|
+
review_status: ""
|
|
313
|
+
reviewed_by: ""
|
|
314
|
+
history:
|
|
315
|
+
- timestamp: "2025-01-01T00:00:00Z"
|
|
316
|
+
lane: "planned"
|
|
317
|
+
agent: "system"
|
|
318
|
+
action: "Prompt generated via /spec-kitty.tasks"
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
# Work Package Prompt: WP01 – Work Package Title
|
|
322
|
+
|
|
323
|
+
[Content follows...]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Valid Lane Values
|
|
327
|
+
|
|
328
|
+
- `planned` - Ready for implementation
|
|
329
|
+
- `doing` - Currently being worked on
|
|
330
|
+
- `for_review` - Awaiting review
|
|
331
|
+
- `done` - Completed
|
|
332
|
+
|
|
333
|
+
## Moving Between Lanes
|
|
334
|
+
|
|
335
|
+
Use the CLI (updates frontmatter only, no file movement):
|
|
336
|
+
```bash
|
|
337
|
+
spec-kitty agent tasks move-task <WPID> --to <lane>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
```bash
|
|
342
|
+
spec-kitty agent tasks move-task WP01 --to doing
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## File Naming
|
|
346
|
+
|
|
347
|
+
- Format: `WP01-kebab-case-slug.md`
|
|
348
|
+
- Examples: `WP01-setup-infrastructure.md`, `WP02-user-auth.md`
|
|
349
|
+
'''
|
|
350
|
+
(tasks_dir / "README.md").write_text(tasks_readme_content)
|
|
351
|
+
|
|
352
|
+
# Create worktree .kittify directory if it doesn't exist
|
|
353
|
+
worktree_kittify = worktree_path / ".kittify"
|
|
354
|
+
worktree_kittify.mkdir(exist_ok=True)
|
|
355
|
+
|
|
356
|
+
# Setup shared constitution and AGENTS.md via symlink (or copy on Windows)
|
|
357
|
+
# Calculate relative path from worktree to main repo
|
|
358
|
+
# Worktree: .worktrees/001-feature/.kittify/memory
|
|
359
|
+
# Main: .kittify/memory
|
|
360
|
+
# Relative: ../../../.kittify/memory
|
|
361
|
+
relative_memory_path = Path("../../../.kittify/memory")
|
|
362
|
+
relative_agents_path = Path("../../../.kittify/AGENTS.md")
|
|
363
|
+
|
|
364
|
+
worktree_memory = worktree_kittify / "memory"
|
|
365
|
+
worktree_agents = worktree_kittify / "AGENTS.md"
|
|
366
|
+
|
|
367
|
+
# Detect if we're on Windows or symlinks are not supported
|
|
368
|
+
is_windows = platform.system() == "Windows"
|
|
369
|
+
use_copy = is_windows or not create_symlinks
|
|
370
|
+
|
|
371
|
+
# Setup memory/ symlink or copy
|
|
372
|
+
if worktree_memory.is_symlink():
|
|
373
|
+
# Remove existing symlink first (can't use rmtree on symlinks)
|
|
374
|
+
worktree_memory.unlink()
|
|
375
|
+
elif worktree_memory.exists() and worktree_memory.is_dir():
|
|
376
|
+
# Remove existing directory (from git worktree add)
|
|
377
|
+
shutil.rmtree(worktree_memory)
|
|
378
|
+
|
|
379
|
+
if use_copy:
|
|
380
|
+
# Copy memory directory
|
|
381
|
+
main_memory = repo_root / ".kittify" / "memory"
|
|
382
|
+
if main_memory.exists() and main_memory.is_dir():
|
|
383
|
+
shutil.copytree(main_memory, worktree_memory)
|
|
384
|
+
else:
|
|
385
|
+
# Create relative symlink
|
|
386
|
+
try:
|
|
387
|
+
worktree_memory.symlink_to(relative_memory_path, target_is_directory=True)
|
|
388
|
+
except (OSError, NotImplementedError):
|
|
389
|
+
# Symlink failed, fall back to copy
|
|
390
|
+
main_memory = repo_root / ".kittify" / "memory"
|
|
391
|
+
if main_memory.exists() and main_memory.is_dir():
|
|
392
|
+
shutil.copytree(main_memory, worktree_memory)
|
|
393
|
+
|
|
394
|
+
# Setup AGENTS.md symlink or copy
|
|
395
|
+
if worktree_agents.exists():
|
|
396
|
+
worktree_agents.unlink()
|
|
397
|
+
|
|
398
|
+
main_agents = repo_root / ".kittify" / "AGENTS.md"
|
|
399
|
+
if main_agents.exists():
|
|
400
|
+
if use_copy:
|
|
401
|
+
shutil.copy2(main_agents, worktree_agents)
|
|
402
|
+
else:
|
|
403
|
+
try:
|
|
404
|
+
worktree_agents.symlink_to(relative_agents_path)
|
|
405
|
+
except (OSError, NotImplementedError):
|
|
406
|
+
shutil.copy2(main_agents, worktree_agents)
|
|
407
|
+
|
|
408
|
+
# Exclude symlinks from git to prevent them from being committed
|
|
409
|
+
# This fixes issue #79: symlinks overwriting main repo files on merge
|
|
410
|
+
_exclude_from_git(worktree_path, [".kittify/memory", ".kittify/AGENTS.md"])
|
|
411
|
+
|
|
412
|
+
# Copy spec template if it exists
|
|
413
|
+
spec_file = feature_dir / "spec.md"
|
|
414
|
+
if not spec_file.exists():
|
|
415
|
+
# Try to find spec template
|
|
416
|
+
spec_template_candidates = [
|
|
417
|
+
repo_root / ".kittify" / "templates" / "spec-template.md",
|
|
418
|
+
repo_root / "templates" / "spec-template.md",
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
for template in spec_template_candidates:
|
|
422
|
+
if template.exists():
|
|
423
|
+
shutil.copy2(template, spec_file)
|
|
424
|
+
break
|
|
425
|
+
else:
|
|
426
|
+
# No template found, create empty spec.md
|
|
427
|
+
spec_file.touch()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def validate_feature_structure(
|
|
431
|
+
feature_dir: Path,
|
|
432
|
+
check_tasks: bool = False
|
|
433
|
+
) -> dict:
|
|
434
|
+
"""Validate feature directory structure and required files.
|
|
435
|
+
|
|
436
|
+
Checks for:
|
|
437
|
+
- Required files: spec.md
|
|
438
|
+
- Recommended directories: checklists/, research/, tasks/
|
|
439
|
+
- Optional: tasks.md (if check_tasks=True)
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
feature_dir: Feature directory path
|
|
443
|
+
check_tasks: If True, validate tasks.md and task files exist
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Dictionary with validation results:
|
|
447
|
+
{
|
|
448
|
+
"valid": bool,
|
|
449
|
+
"errors": [list of error messages],
|
|
450
|
+
"warnings": [list of warning messages],
|
|
451
|
+
"paths": {dict of important paths}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
Examples:
|
|
455
|
+
>>> feature_dir = Path("/path/to/kitty-specs/001-feature")
|
|
456
|
+
>>> result = validate_feature_structure(feature_dir)
|
|
457
|
+
>>> assert "valid" in result
|
|
458
|
+
>>> assert "errors" in result
|
|
459
|
+
"""
|
|
460
|
+
errors = []
|
|
461
|
+
warnings = []
|
|
462
|
+
paths = {}
|
|
463
|
+
|
|
464
|
+
# Check if feature directory exists
|
|
465
|
+
if not feature_dir.exists():
|
|
466
|
+
errors.append(f"Feature directory not found: {feature_dir}")
|
|
467
|
+
return {
|
|
468
|
+
"valid": False,
|
|
469
|
+
"errors": errors,
|
|
470
|
+
"warnings": warnings,
|
|
471
|
+
"paths": paths
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
# Check required files exist
|
|
475
|
+
spec_file = feature_dir / "spec.md"
|
|
476
|
+
if not spec_file.exists():
|
|
477
|
+
errors.append("Missing required file: spec.md")
|
|
478
|
+
else:
|
|
479
|
+
paths["spec_file"] = str(spec_file)
|
|
480
|
+
|
|
481
|
+
# Check directory structure
|
|
482
|
+
recommended_dirs = ["checklists", "research", "tasks"]
|
|
483
|
+
for dir_name in recommended_dirs:
|
|
484
|
+
dir_path = feature_dir / dir_name
|
|
485
|
+
if not dir_path.exists():
|
|
486
|
+
warnings.append(f"Missing recommended directory: {dir_name}/")
|
|
487
|
+
else:
|
|
488
|
+
paths[f"{dir_name}_dir"] = str(dir_path)
|
|
489
|
+
|
|
490
|
+
# Check task files if requested
|
|
491
|
+
if check_tasks:
|
|
492
|
+
tasks_file = feature_dir / "tasks.md"
|
|
493
|
+
if not tasks_file.exists():
|
|
494
|
+
errors.append("Missing required file: tasks.md")
|
|
495
|
+
else:
|
|
496
|
+
paths["tasks_file"] = str(tasks_file)
|
|
497
|
+
|
|
498
|
+
# Always include feature_dir in paths
|
|
499
|
+
paths["feature_dir"] = str(feature_dir)
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
"valid": len(errors) == 0,
|
|
503
|
+
"errors": errors,
|
|
504
|
+
"warnings": warnings,
|
|
505
|
+
"paths": paths
|
|
506
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Dashboard package public API."""
|
|
2
|
+
|
|
3
|
+
from .diagnostics import run_diagnostics
|
|
4
|
+
from .lifecycle import ensure_dashboard_running, stop_dashboard
|
|
5
|
+
from .scanner import (
|
|
6
|
+
format_path_for_display,
|
|
7
|
+
get_feature_artifacts,
|
|
8
|
+
get_workflow_status,
|
|
9
|
+
resolve_feature_dir,
|
|
10
|
+
scan_all_features,
|
|
11
|
+
scan_feature_kanban,
|
|
12
|
+
)
|
|
13
|
+
from .server import find_free_port, run_dashboard_server, start_dashboard
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ensure_dashboard_running",
|
|
17
|
+
"stop_dashboard",
|
|
18
|
+
"find_free_port",
|
|
19
|
+
"start_dashboard",
|
|
20
|
+
"run_dashboard_server",
|
|
21
|
+
"scan_all_features",
|
|
22
|
+
"scan_feature_kanban",
|
|
23
|
+
"get_feature_artifacts",
|
|
24
|
+
"get_workflow_status",
|
|
25
|
+
"resolve_feature_dir",
|
|
26
|
+
"format_path_for_display",
|
|
27
|
+
"run_diagnostics",
|
|
28
|
+
]
|