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,973 @@
|
|
|
1
|
+
"""Implement command - create workspace for work package implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from specify_cli.cli import StepTracker
|
|
15
|
+
from specify_cli.core.dependency_graph import (
|
|
16
|
+
build_dependency_graph,
|
|
17
|
+
get_dependents,
|
|
18
|
+
parse_wp_dependencies,
|
|
19
|
+
)
|
|
20
|
+
from specify_cli.core.vcs import (
|
|
21
|
+
get_vcs,
|
|
22
|
+
VCSBackend,
|
|
23
|
+
VCSLockError,
|
|
24
|
+
)
|
|
25
|
+
from specify_cli.frontmatter import read_frontmatter, update_fields
|
|
26
|
+
from specify_cli.tasks_support import (
|
|
27
|
+
TaskCliError,
|
|
28
|
+
find_repo_root,
|
|
29
|
+
locate_work_package,
|
|
30
|
+
set_scalar,
|
|
31
|
+
build_document,
|
|
32
|
+
)
|
|
33
|
+
from specify_cli.workspace_context import WorkspaceContext, save_context
|
|
34
|
+
from specify_cli.core.multi_parent_merge import create_multi_parent_base
|
|
35
|
+
from specify_cli.core.context_validation import require_main_repo
|
|
36
|
+
|
|
37
|
+
console = Console()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detect_feature_context(feature_flag: str | None = None) -> tuple[str, str]:
|
|
41
|
+
"""Detect feature number and slug from current context.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
feature_flag: Explicit feature slug from --feature flag (optional)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of (feature_number, feature_slug)
|
|
48
|
+
Example: ("010", "010-workspace-per-wp")
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
typer.Exit: If feature context cannot be detected
|
|
52
|
+
"""
|
|
53
|
+
# Priority 1: Explicit --feature flag
|
|
54
|
+
if feature_flag:
|
|
55
|
+
match = re.match(r'^(\d{3})-(.+)$', feature_flag)
|
|
56
|
+
if match:
|
|
57
|
+
number = match.group(1)
|
|
58
|
+
return number, feature_flag
|
|
59
|
+
else:
|
|
60
|
+
console.print(f"[red]Error:[/red] Invalid feature format: {feature_flag}")
|
|
61
|
+
console.print("Expected format: ###-feature-name (e.g., 001-my-feature)")
|
|
62
|
+
raise typer.Exit(1)
|
|
63
|
+
|
|
64
|
+
# Priority 2: Try git branch
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
check=False
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if result.returncode == 0:
|
|
73
|
+
branch = result.stdout.strip()
|
|
74
|
+
|
|
75
|
+
# Pattern 1: WP branch (###-feature-name-WP##)
|
|
76
|
+
# Check this FIRST - more specific pattern
|
|
77
|
+
# Extract feature slug by removing -WP## suffix
|
|
78
|
+
match = re.match(r'^((\d{3})-.+)-WP\d{2}$', branch)
|
|
79
|
+
if match:
|
|
80
|
+
slug = match.group(1)
|
|
81
|
+
number = match.group(2)
|
|
82
|
+
return number, slug
|
|
83
|
+
|
|
84
|
+
# Pattern 2: Feature branch (###-feature-name)
|
|
85
|
+
match = re.match(r'^(\d{3})-(.+)$', branch)
|
|
86
|
+
if match:
|
|
87
|
+
number = match.group(1)
|
|
88
|
+
slug = branch
|
|
89
|
+
return number, slug
|
|
90
|
+
|
|
91
|
+
# Try current directory
|
|
92
|
+
cwd = Path.cwd()
|
|
93
|
+
# Look for kitty-specs/###-feature-name/ in path
|
|
94
|
+
for part in cwd.parts:
|
|
95
|
+
match = re.match(r'^(\d{3})-(.+)$', part)
|
|
96
|
+
if match:
|
|
97
|
+
number = match.group(1)
|
|
98
|
+
slug = part
|
|
99
|
+
return number, slug
|
|
100
|
+
|
|
101
|
+
# Try scanning kitty-specs/ for features (v0.11.0 workflow)
|
|
102
|
+
try:
|
|
103
|
+
repo_root = find_repo_root()
|
|
104
|
+
kitty_specs = repo_root / "kitty-specs"
|
|
105
|
+
if kitty_specs.exists():
|
|
106
|
+
# Find all feature directories
|
|
107
|
+
features = [
|
|
108
|
+
d.name for d in kitty_specs.iterdir()
|
|
109
|
+
if d.is_dir() and re.match(r'^\d{3}-', d.name)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
if len(features) == 1:
|
|
113
|
+
# Only one feature - use it automatically
|
|
114
|
+
match = re.match(r'^(\d{3})-(.+)$', features[0])
|
|
115
|
+
if match:
|
|
116
|
+
number = match.group(1)
|
|
117
|
+
slug = features[0]
|
|
118
|
+
return number, slug
|
|
119
|
+
elif len(features) > 1:
|
|
120
|
+
# Multiple features - need user to specify
|
|
121
|
+
console.print("[red]Error:[/red] Multiple features found:")
|
|
122
|
+
for f in sorted(features):
|
|
123
|
+
console.print(f" - {f}")
|
|
124
|
+
console.print("\nSpecify feature explicitly:")
|
|
125
|
+
console.print(" spec-kitty implement WP01 --feature 001-my-feature")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
except TaskCliError:
|
|
128
|
+
# Not in a git repo, continue to generic error
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# Cannot detect
|
|
132
|
+
console.print("[red]Error:[/red] Cannot detect feature context")
|
|
133
|
+
console.print("Run this command from a feature branch or feature directory")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def find_wp_file(repo_root: Path, feature_slug: str, wp_id: str) -> Path:
|
|
138
|
+
"""Find WP file in kitty-specs/###-feature/tasks/ directory.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
repo_root: Repository root path
|
|
142
|
+
feature_slug: Feature slug (e.g., "010-workspace-per-wp")
|
|
143
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Path to WP file
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
FileNotFoundError: If WP file not found
|
|
150
|
+
"""
|
|
151
|
+
tasks_dir = repo_root / "kitty-specs" / feature_slug / "tasks"
|
|
152
|
+
if not tasks_dir.exists():
|
|
153
|
+
raise FileNotFoundError(f"Tasks directory not found: {tasks_dir}")
|
|
154
|
+
|
|
155
|
+
# Search for WP##-*.md pattern
|
|
156
|
+
wp_files = list(tasks_dir.glob(f"{wp_id}-*.md"))
|
|
157
|
+
if not wp_files:
|
|
158
|
+
raise FileNotFoundError(f"WP file not found for {wp_id} in {tasks_dir}")
|
|
159
|
+
|
|
160
|
+
return wp_files[0]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_workspace_path(workspace_path: Path, wp_id: str) -> bool:
|
|
164
|
+
"""Ensure workspace path is available or reusable.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
workspace_path: Path to workspace directory
|
|
168
|
+
wp_id: Work package ID
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if workspace already exists and is valid (reusable)
|
|
172
|
+
False if workspace doesn't exist (should create)
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
typer.Exit: If directory exists but is not a valid worktree
|
|
176
|
+
"""
|
|
177
|
+
if not workspace_path.exists():
|
|
178
|
+
return False # Good - doesn't exist, should create
|
|
179
|
+
|
|
180
|
+
# Check if it's a valid git worktree
|
|
181
|
+
result = subprocess.run(
|
|
182
|
+
["git", "rev-parse", "--git-dir"],
|
|
183
|
+
cwd=workspace_path,
|
|
184
|
+
capture_output=True,
|
|
185
|
+
check=False
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if result.returncode == 0:
|
|
189
|
+
# Valid worktree exists
|
|
190
|
+
console.print(f"[cyan]Workspace for {wp_id} already exists[/cyan]")
|
|
191
|
+
console.print(f"Reusing: {workspace_path}")
|
|
192
|
+
|
|
193
|
+
# SECURITY CHECK: Detect symlinks to kitty-specs/ (bypass attempt)
|
|
194
|
+
kitty_specs_path = workspace_path / "kitty-specs"
|
|
195
|
+
if kitty_specs_path.is_symlink():
|
|
196
|
+
console.print()
|
|
197
|
+
console.print("[bold red]⚠️ SECURITY WARNING: kitty-specs/ is a symlink![/bold red]")
|
|
198
|
+
console.print(f" Target: {kitty_specs_path.resolve()}")
|
|
199
|
+
console.print(" This bypasses sparse-checkout isolation and can corrupt main repo state.")
|
|
200
|
+
console.print(f" Remove with: rm {kitty_specs_path}")
|
|
201
|
+
console.print()
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
return True # Reuse existing
|
|
205
|
+
|
|
206
|
+
# Directory exists but not a worktree
|
|
207
|
+
console.print(f"[red]Error:[/red] Directory exists but is not a valid worktree")
|
|
208
|
+
console.print(f"Path: {workspace_path}")
|
|
209
|
+
console.print(f"Remove manually: rm -rf {workspace_path}")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_base_branch_changed(workspace_path: Path, base_branch: str) -> bool:
|
|
214
|
+
"""Check if base branch has commits not in current workspace.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
workspace_path: Path to workspace directory
|
|
218
|
+
base_branch: Base branch name (e.g., "010-workspace-per-wp-WP01")
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if base branch has new commits not in workspace
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
# Get merge-base (common ancestor between workspace and base)
|
|
225
|
+
result = subprocess.run(
|
|
226
|
+
["git", "merge-base", "HEAD", base_branch],
|
|
227
|
+
cwd=workspace_path,
|
|
228
|
+
capture_output=True,
|
|
229
|
+
text=True,
|
|
230
|
+
check=False,
|
|
231
|
+
)
|
|
232
|
+
if result.returncode != 0:
|
|
233
|
+
# Cannot determine merge-base (branches diverged too much or other issue)
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
merge_base = result.stdout.strip()
|
|
237
|
+
|
|
238
|
+
# Get base branch tip
|
|
239
|
+
result = subprocess.run(
|
|
240
|
+
["git", "rev-parse", base_branch],
|
|
241
|
+
cwd=workspace_path,
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
check=False,
|
|
245
|
+
)
|
|
246
|
+
if result.returncode != 0:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
base_tip = result.stdout.strip()
|
|
250
|
+
|
|
251
|
+
# If merge-base != base tip, base has new commits
|
|
252
|
+
return merge_base != base_tip
|
|
253
|
+
|
|
254
|
+
except Exception:
|
|
255
|
+
# If git commands fail, assume no changes
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def resolve_primary_branch(repo_root: Path) -> str:
|
|
260
|
+
"""Resolve the primary branch name (main or master).
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
"main" if it exists, otherwise "master" if it exists.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
typer.Exit: If neither branch exists.
|
|
267
|
+
"""
|
|
268
|
+
for candidate in ("main", "master"):
|
|
269
|
+
result = subprocess.run(
|
|
270
|
+
["git", "rev-parse", "--verify", candidate],
|
|
271
|
+
cwd=repo_root,
|
|
272
|
+
capture_output=True,
|
|
273
|
+
check=False,
|
|
274
|
+
)
|
|
275
|
+
if result.returncode == 0:
|
|
276
|
+
return candidate
|
|
277
|
+
|
|
278
|
+
console.print("[red]Error:[/red] Neither 'main' nor 'master' branch exists.")
|
|
279
|
+
raise typer.Exit(1)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def display_rebase_warning(
|
|
283
|
+
workspace_path: Path,
|
|
284
|
+
wp_id: str,
|
|
285
|
+
base_branch: str,
|
|
286
|
+
feature_slug: str
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Display warning about needing to rebase on changed base.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
workspace_path: Path to workspace directory
|
|
292
|
+
wp_id: Work package ID (e.g., "WP02")
|
|
293
|
+
base_branch: Base branch name (e.g., "010-workspace-per-wp-WP01")
|
|
294
|
+
feature_slug: Feature slug (e.g., "010-workspace-per-wp")
|
|
295
|
+
"""
|
|
296
|
+
console.print(f"\n[bold yellow]⚠️ Base branch {base_branch} has changed[/bold yellow]")
|
|
297
|
+
console.print(f"Your {wp_id} workspace may have outdated code from base\n")
|
|
298
|
+
|
|
299
|
+
console.print("[cyan]Recommended action:[/cyan]")
|
|
300
|
+
console.print(f" cd {workspace_path}")
|
|
301
|
+
console.print(f" git rebase {base_branch}")
|
|
302
|
+
console.print(" # Resolve any conflicts")
|
|
303
|
+
console.print(" git add .")
|
|
304
|
+
console.print(" git rebase --continue\n")
|
|
305
|
+
|
|
306
|
+
console.print("[yellow]This is a git limitation.[/yellow]")
|
|
307
|
+
console.print("Future jj integration will auto-rebase dependent workspaces.\n")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def check_for_dependents(
|
|
311
|
+
repo_root: Path,
|
|
312
|
+
feature_slug: str,
|
|
313
|
+
wp_id: str
|
|
314
|
+
) -> None:
|
|
315
|
+
"""Check if any WPs depend on this WP and warn if not yet done.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
repo_root: Repository root path
|
|
319
|
+
feature_slug: Feature slug (e.g., "010-workspace-per-wp")
|
|
320
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
321
|
+
"""
|
|
322
|
+
feature_dir = repo_root / "kitty-specs" / feature_slug
|
|
323
|
+
|
|
324
|
+
# Build dependency graph
|
|
325
|
+
graph = build_dependency_graph(feature_dir)
|
|
326
|
+
|
|
327
|
+
# Get dependents
|
|
328
|
+
dependents = get_dependents(wp_id, graph)
|
|
329
|
+
if not dependents:
|
|
330
|
+
return # No dependents, no warnings needed
|
|
331
|
+
|
|
332
|
+
# Check if any dependents are incomplete (any lane except done)
|
|
333
|
+
incomplete_deps = []
|
|
334
|
+
for dep_id in dependents:
|
|
335
|
+
try:
|
|
336
|
+
dep_file = find_wp_file(repo_root, feature_slug, dep_id)
|
|
337
|
+
frontmatter, _ = read_frontmatter(dep_file)
|
|
338
|
+
lane = frontmatter.get("lane", "planned")
|
|
339
|
+
|
|
340
|
+
if lane in ["planned", "doing", "for_review"]:
|
|
341
|
+
incomplete_deps.append(dep_id)
|
|
342
|
+
except (FileNotFoundError, Exception):
|
|
343
|
+
# If we can't read the dependent's metadata, skip it
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
if incomplete_deps:
|
|
347
|
+
console.print(f"\n[yellow]⚠️ Dependency Alert:[/yellow]")
|
|
348
|
+
console.print(f"{', '.join(incomplete_deps)} depend on {wp_id} (not yet done)")
|
|
349
|
+
console.print("If you modify this WP, dependent WPs will need manual rebase:")
|
|
350
|
+
for dep_id in incomplete_deps:
|
|
351
|
+
dep_workspace = f".worktrees/{feature_slug}-{dep_id}"
|
|
352
|
+
console.print(f" cd {dep_workspace} && git rebase {feature_slug}-{wp_id}")
|
|
353
|
+
console.print()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _ensure_planning_artifacts_committed_git(
|
|
357
|
+
repo_root: Path,
|
|
358
|
+
feature_dir: Path,
|
|
359
|
+
feature_slug: str,
|
|
360
|
+
wp_id: str,
|
|
361
|
+
primary_branch: str,
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Ensure planning artifacts are committed using git commands.
|
|
364
|
+
|
|
365
|
+
For git repos, checks that:
|
|
366
|
+
1. We're on the primary branch (main/master)
|
|
367
|
+
2. No uncommitted files exist in kitty-specs/$feature/
|
|
368
|
+
|
|
369
|
+
If uncommitted files exist and we're on the primary branch, auto-commits them.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
repo_root: Repository root path
|
|
373
|
+
feature_dir: Path to feature directory (kitty-specs/###-feature/)
|
|
374
|
+
feature_slug: Feature slug (e.g., "001-my-feature")
|
|
375
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
376
|
+
primary_branch: Primary branch name (main/master)
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
typer.Exit: If not on primary branch or commit fails
|
|
380
|
+
"""
|
|
381
|
+
# Check current branch
|
|
382
|
+
result = subprocess.run(
|
|
383
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
384
|
+
cwd=repo_root,
|
|
385
|
+
capture_output=True,
|
|
386
|
+
text=True,
|
|
387
|
+
check=False
|
|
388
|
+
)
|
|
389
|
+
current_branch = result.stdout.strip() if result.returncode == 0 else ""
|
|
390
|
+
|
|
391
|
+
# Check git status for untracked/modified files in feature directory
|
|
392
|
+
result = subprocess.run(
|
|
393
|
+
["git", "status", "--porcelain", str(feature_dir)],
|
|
394
|
+
cwd=repo_root,
|
|
395
|
+
capture_output=True,
|
|
396
|
+
text=True,
|
|
397
|
+
check=False
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
401
|
+
# Parse git status output - any file showing up needs to be committed
|
|
402
|
+
# Porcelain format: XY filename (X=staged, Y=working tree)
|
|
403
|
+
# Examples: ??(untracked), M (staged modified), MM(staged+modified), etc.
|
|
404
|
+
files_to_commit = []
|
|
405
|
+
for line in result.stdout.strip().split('\n'):
|
|
406
|
+
if line.strip():
|
|
407
|
+
# Get status code (first 2 chars) and filepath (rest after space)
|
|
408
|
+
if len(line) >= 3:
|
|
409
|
+
filepath = line[3:].strip()
|
|
410
|
+
# Any file with status means it's untracked, modified, or staged
|
|
411
|
+
# All of these should be included in the commit
|
|
412
|
+
files_to_commit.append(filepath)
|
|
413
|
+
|
|
414
|
+
if files_to_commit:
|
|
415
|
+
console.print(f"\n[cyan]Planning artifacts not committed:[/cyan]")
|
|
416
|
+
for f in files_to_commit:
|
|
417
|
+
console.print(f" {f}")
|
|
418
|
+
|
|
419
|
+
if current_branch != primary_branch:
|
|
420
|
+
console.print(
|
|
421
|
+
f"\n[red]Error:[/red] Planning artifacts must be committed on {primary_branch}."
|
|
422
|
+
)
|
|
423
|
+
console.print(f"Current branch: {current_branch}")
|
|
424
|
+
console.print(f"Run: git checkout {primary_branch}")
|
|
425
|
+
raise typer.Exit(1)
|
|
426
|
+
|
|
427
|
+
console.print(f"\n[cyan]Auto-committing to {primary_branch}...[/cyan]")
|
|
428
|
+
|
|
429
|
+
# Stage all files in feature directory
|
|
430
|
+
# Use -f to force-add files in kitty-specs/ which is in .gitignore
|
|
431
|
+
result = subprocess.run(
|
|
432
|
+
["git", "add", "-f", str(feature_dir)],
|
|
433
|
+
cwd=repo_root,
|
|
434
|
+
capture_output=True,
|
|
435
|
+
text=True,
|
|
436
|
+
check=False
|
|
437
|
+
)
|
|
438
|
+
if result.returncode != 0:
|
|
439
|
+
console.print(f"[red]Error:[/red] Failed to stage files")
|
|
440
|
+
console.print(result.stderr)
|
|
441
|
+
raise typer.Exit(1)
|
|
442
|
+
|
|
443
|
+
# Commit with descriptive message
|
|
444
|
+
commit_msg = f"chore: Planning artifacts for {feature_slug}\n\nAuto-committed by spec-kitty before creating workspace for {wp_id}"
|
|
445
|
+
result = subprocess.run(
|
|
446
|
+
["git", "commit", "-m", commit_msg],
|
|
447
|
+
cwd=repo_root,
|
|
448
|
+
capture_output=True,
|
|
449
|
+
text=True,
|
|
450
|
+
check=False
|
|
451
|
+
)
|
|
452
|
+
if result.returncode != 0:
|
|
453
|
+
console.print(f"[red]Error:[/red] Failed to commit")
|
|
454
|
+
console.print(result.stderr)
|
|
455
|
+
raise typer.Exit(1)
|
|
456
|
+
|
|
457
|
+
console.print(f"[green]✓[/green] Planning artifacts committed to {primary_branch}")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _ensure_planning_artifacts_committed_jj(
|
|
461
|
+
repo_root: Path,
|
|
462
|
+
feature_dir: Path,
|
|
463
|
+
feature_slug: str,
|
|
464
|
+
wp_id: str,
|
|
465
|
+
primary_branch: str,
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Verify planning artifacts exist for jj repos.
|
|
468
|
+
|
|
469
|
+
For jj repos, the working copy IS always a commit - there's no "uncommitted"
|
|
470
|
+
state like in git. We just need to verify the feature directory exists.
|
|
471
|
+
|
|
472
|
+
The user can run orchestration from any bookmark - we don't enforce being
|
|
473
|
+
on main. The planning artifacts just need to exist in the current revision.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
repo_root: Repository root path
|
|
477
|
+
feature_dir: Path to feature directory (kitty-specs/###-feature/)
|
|
478
|
+
feature_slug: Feature slug (e.g., "001-my-feature")
|
|
479
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
480
|
+
primary_branch: Primary branch name (main/master) - not enforced
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
typer.Exit: If feature directory doesn't exist
|
|
484
|
+
"""
|
|
485
|
+
# In jj, working copy IS a commit - no "uncommitted" state
|
|
486
|
+
# Just verify the feature directory exists
|
|
487
|
+
if not feature_dir.exists():
|
|
488
|
+
console.print(
|
|
489
|
+
f"\n[red]Error:[/red] Feature directory not found: {feature_dir}"
|
|
490
|
+
)
|
|
491
|
+
console.print("Run planning commands first (specify, plan, tasks)")
|
|
492
|
+
raise typer.Exit(1)
|
|
493
|
+
|
|
494
|
+
# Get current bookmark for display
|
|
495
|
+
result = subprocess.run(
|
|
496
|
+
["jj", "log", "-r", "@", "--no-graph", "-T", "bookmarks"],
|
|
497
|
+
cwd=repo_root,
|
|
498
|
+
capture_output=True,
|
|
499
|
+
text=True,
|
|
500
|
+
check=False
|
|
501
|
+
)
|
|
502
|
+
current_bookmark = result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
503
|
+
console.print(f"[green]✓[/green] Planning artifacts ready (on {current_bookmark or '@'})")
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _ensure_vcs_in_meta(feature_dir: Path, repo_root: Path) -> VCSBackend:
|
|
507
|
+
"""Ensure VCS is selected and locked in meta.json.
|
|
508
|
+
|
|
509
|
+
Always locks to git (jj support removed due to sparse checkout incompatibility).
|
|
510
|
+
|
|
511
|
+
If a feature was created with jj, it will be automatically converted to git
|
|
512
|
+
with a warning message.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
feature_dir: Path to the feature directory (kitty-specs/###-feature/)
|
|
516
|
+
repo_root: Repository root path (not used, but kept for compatibility)
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
VCSBackend.GIT (always)
|
|
520
|
+
|
|
521
|
+
Raises:
|
|
522
|
+
typer.Exit: If meta.json is missing or malformed
|
|
523
|
+
"""
|
|
524
|
+
meta_path = feature_dir / "meta.json"
|
|
525
|
+
|
|
526
|
+
if not meta_path.exists():
|
|
527
|
+
console.print(f"[red]Error:[/red] meta.json not found in {feature_dir}")
|
|
528
|
+
console.print("Run /spec-kitty.specify first to create feature structure")
|
|
529
|
+
raise typer.Exit(1)
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
533
|
+
except json.JSONDecodeError as e:
|
|
534
|
+
console.print(f"[red]Error:[/red] Invalid JSON in meta.json: {e}")
|
|
535
|
+
raise typer.Exit(1)
|
|
536
|
+
|
|
537
|
+
# Check if VCS is already locked
|
|
538
|
+
if "vcs" in meta:
|
|
539
|
+
backend_str = meta["vcs"]
|
|
540
|
+
if backend_str == "jj":
|
|
541
|
+
console.print("[yellow]Warning:[/yellow] Feature was created with jj, but jj is no longer supported.")
|
|
542
|
+
console.print("[yellow]Converting to git...[/yellow]")
|
|
543
|
+
# Override to git
|
|
544
|
+
meta["vcs"] = "git"
|
|
545
|
+
meta["vcs_locked_at"] = datetime.now(timezone.utc).isoformat()
|
|
546
|
+
meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
547
|
+
return VCSBackend.GIT
|
|
548
|
+
# Already git
|
|
549
|
+
return VCSBackend.GIT
|
|
550
|
+
|
|
551
|
+
# VCS not yet locked - lock to git (only supported VCS)
|
|
552
|
+
meta["vcs"] = "git"
|
|
553
|
+
meta["vcs_locked_at"] = datetime.now(timezone.utc).isoformat()
|
|
554
|
+
|
|
555
|
+
# Write updated meta.json
|
|
556
|
+
meta_path.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
557
|
+
|
|
558
|
+
console.print("[cyan]→ VCS locked to git in meta.json[/cyan]")
|
|
559
|
+
return VCSBackend.GIT
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@require_main_repo
|
|
563
|
+
def implement(
|
|
564
|
+
wp_id: str = typer.Argument(..., help="Work package ID (e.g., WP01)"),
|
|
565
|
+
base: str = typer.Option(None, "--base", help="Base WP to branch from (e.g., WP01)"),
|
|
566
|
+
feature: str = typer.Option(None, "--feature", help="Feature slug (e.g., 001-my-feature)"),
|
|
567
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Create workspace for work package implementation.
|
|
570
|
+
|
|
571
|
+
Creates a git worktree for the specified work package, branching from
|
|
572
|
+
main (for WPs with no dependencies) or from a base WP's branch.
|
|
573
|
+
|
|
574
|
+
Examples:
|
|
575
|
+
# Create workspace for WP01 (no dependencies)
|
|
576
|
+
spec-kitty implement WP01
|
|
577
|
+
|
|
578
|
+
# Create workspace for WP02, branching from WP01
|
|
579
|
+
spec-kitty implement WP02 --base WP01
|
|
580
|
+
|
|
581
|
+
# Explicit feature specification
|
|
582
|
+
spec-kitty implement WP01 --feature 001-my-feature
|
|
583
|
+
|
|
584
|
+
# JSON output for scripting
|
|
585
|
+
spec-kitty implement WP01 --json
|
|
586
|
+
"""
|
|
587
|
+
# Context validation handled by @require_main_repo decorator
|
|
588
|
+
tracker = StepTracker(f"Implement {wp_id}")
|
|
589
|
+
tracker.add("detect", "Detect feature context")
|
|
590
|
+
tracker.add("validate", "Validate dependencies")
|
|
591
|
+
tracker.add("create", "Create workspace")
|
|
592
|
+
console.print()
|
|
593
|
+
|
|
594
|
+
# Step 1: Detect feature context
|
|
595
|
+
tracker.start("detect")
|
|
596
|
+
try:
|
|
597
|
+
repo_root = find_repo_root()
|
|
598
|
+
feature_number, feature_slug = detect_feature_context(feature)
|
|
599
|
+
tracker.complete("detect", f"Feature: {feature_slug}")
|
|
600
|
+
except (TaskCliError, typer.Exit) as exc:
|
|
601
|
+
tracker.error("detect", str(exc) if isinstance(exc, TaskCliError) else "failed")
|
|
602
|
+
console.print(tracker.render())
|
|
603
|
+
raise typer.Exit(1)
|
|
604
|
+
|
|
605
|
+
# Step 2: Validate dependencies
|
|
606
|
+
tracker.start("validate")
|
|
607
|
+
auto_merge_base = False # Track if we're using auto-merge
|
|
608
|
+
try:
|
|
609
|
+
# Find WP file to read dependencies
|
|
610
|
+
wp_file = find_wp_file(repo_root, feature_slug, wp_id)
|
|
611
|
+
declared_deps = parse_wp_dependencies(wp_file)
|
|
612
|
+
|
|
613
|
+
# Multi-parent dependency handling
|
|
614
|
+
if len(declared_deps) > 1 and base is None:
|
|
615
|
+
# Auto-merge mode: Create merge commit combining all dependencies
|
|
616
|
+
console.print(f"\n[cyan]Multi-parent dependency detected:[/cyan]")
|
|
617
|
+
console.print(f" {wp_id} depends on: {', '.join(declared_deps)}")
|
|
618
|
+
console.print(f" Auto-creating merge base combining all dependencies...")
|
|
619
|
+
auto_merge_base = True
|
|
620
|
+
# Will create merge base after validation completes
|
|
621
|
+
|
|
622
|
+
# Single dependency handling
|
|
623
|
+
elif len(declared_deps) == 1 and base is None:
|
|
624
|
+
# Suggest base for single dependency
|
|
625
|
+
tracker.error("validate", "missing --base flag")
|
|
626
|
+
console.print(tracker.render())
|
|
627
|
+
console.print(f"\n[red]Error:[/red] {wp_id} depends on {declared_deps[0]}")
|
|
628
|
+
console.print(f"\nSpecify base workspace:")
|
|
629
|
+
console.print(f" spec-kitty implement {wp_id} --base {declared_deps[0]}")
|
|
630
|
+
raise typer.Exit(1)
|
|
631
|
+
|
|
632
|
+
# If --base provided, validate it matches declared dependencies
|
|
633
|
+
if base:
|
|
634
|
+
if base not in declared_deps and declared_deps:
|
|
635
|
+
console.print(f"[yellow]Warning:[/yellow] {wp_id} does not declare dependency on {base}")
|
|
636
|
+
console.print(f"Declared dependencies: {declared_deps}")
|
|
637
|
+
# Allow but warn (user might know better than parser)
|
|
638
|
+
|
|
639
|
+
# Validate base workspace exists
|
|
640
|
+
base_workspace = repo_root / ".worktrees" / f"{feature_slug}-{base}"
|
|
641
|
+
if not base_workspace.exists():
|
|
642
|
+
tracker.error("validate", f"base workspace {base} not found")
|
|
643
|
+
console.print(tracker.render())
|
|
644
|
+
console.print(f"\n[red]Error:[/red] Base workspace {base} does not exist")
|
|
645
|
+
console.print(f"Implement {base} first: spec-kitty implement {base}")
|
|
646
|
+
raise typer.Exit(1)
|
|
647
|
+
|
|
648
|
+
# Verify it's a valid worktree
|
|
649
|
+
result = subprocess.run(
|
|
650
|
+
["git", "rev-parse", "--git-dir"],
|
|
651
|
+
cwd=base_workspace,
|
|
652
|
+
capture_output=True,
|
|
653
|
+
check=False
|
|
654
|
+
)
|
|
655
|
+
if result.returncode != 0:
|
|
656
|
+
tracker.error("validate", f"base workspace {base} invalid")
|
|
657
|
+
console.print(tracker.render())
|
|
658
|
+
console.print(f"[red]Error:[/red] {base_workspace} exists but is not a valid worktree")
|
|
659
|
+
raise typer.Exit(1)
|
|
660
|
+
|
|
661
|
+
tracker.complete("validate", f"Base: {base or 'main'}")
|
|
662
|
+
except (FileNotFoundError, typer.Exit) as exc:
|
|
663
|
+
if not isinstance(exc, typer.Exit):
|
|
664
|
+
tracker.error("validate", str(exc))
|
|
665
|
+
console.print(tracker.render())
|
|
666
|
+
raise typer.Exit(1)
|
|
667
|
+
|
|
668
|
+
# Step 2.5: Ensure planning artifacts are committed (v0.11.0 requirement)
|
|
669
|
+
# All planning must happen in primary branch and be committed BEFORE worktree creation
|
|
670
|
+
if base is None: # Only for first WP in feature (branches from main)
|
|
671
|
+
try:
|
|
672
|
+
# Detect VCS backend early to use appropriate commands
|
|
673
|
+
feature_dir = repo_root / "kitty-specs" / feature_slug
|
|
674
|
+
if not feature_dir.exists():
|
|
675
|
+
console.print(f"\n[red]Error:[/red] Feature directory not found: {feature_dir}")
|
|
676
|
+
console.print(f"Run /spec-kitty.specify first")
|
|
677
|
+
raise typer.Exit(1)
|
|
678
|
+
|
|
679
|
+
# Get VCS backend (auto-detect or from meta.json)
|
|
680
|
+
vcs = get_vcs(repo_root)
|
|
681
|
+
vcs_backend = vcs.backend
|
|
682
|
+
|
|
683
|
+
primary_branch = resolve_primary_branch(repo_root)
|
|
684
|
+
|
|
685
|
+
if vcs_backend == VCSBackend.GIT:
|
|
686
|
+
# Git path: check branch and status using git commands
|
|
687
|
+
_ensure_planning_artifacts_committed_git(
|
|
688
|
+
repo_root, feature_dir, feature_slug, wp_id, primary_branch
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
# jj path: check status and commit using jj commands
|
|
692
|
+
_ensure_planning_artifacts_committed_jj(
|
|
693
|
+
repo_root, feature_dir, feature_slug, wp_id, primary_branch
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
except typer.Exit:
|
|
697
|
+
raise
|
|
698
|
+
except Exception as e:
|
|
699
|
+
console.print(f"\n[red]Error:[/red] Failed to validate planning artifacts: {e}")
|
|
700
|
+
raise typer.Exit(1)
|
|
701
|
+
|
|
702
|
+
# Step 3: Create workspace
|
|
703
|
+
tracker.start("create")
|
|
704
|
+
try:
|
|
705
|
+
# Determine workspace path and branch name
|
|
706
|
+
workspace_name = f"{feature_slug}-{wp_id}"
|
|
707
|
+
workspace_path = repo_root / ".worktrees" / workspace_name
|
|
708
|
+
branch_name = workspace_name # Same as workspace dir name
|
|
709
|
+
|
|
710
|
+
# Ensure VCS is locked in meta.json and get the backend to use
|
|
711
|
+
# (do this early so we can use VCS for all operations)
|
|
712
|
+
feature_dir = repo_root / "kitty-specs" / feature_slug
|
|
713
|
+
vcs_backend = _ensure_vcs_in_meta(feature_dir, repo_root)
|
|
714
|
+
|
|
715
|
+
# Get VCS implementation
|
|
716
|
+
vcs = get_vcs(repo_root, backend=vcs_backend)
|
|
717
|
+
|
|
718
|
+
# Check if workspace already exists using VCS abstraction
|
|
719
|
+
workspace_info = vcs.get_workspace_info(workspace_path)
|
|
720
|
+
if workspace_info is not None:
|
|
721
|
+
# Workspace exists and is valid, reuse it
|
|
722
|
+
tracker.complete("create", f"Reused: {workspace_path}")
|
|
723
|
+
console.print(tracker.render())
|
|
724
|
+
|
|
725
|
+
# Use VCS abstraction for stale detection
|
|
726
|
+
if workspace_info.is_stale:
|
|
727
|
+
if base:
|
|
728
|
+
base_branch = f"{feature_slug}-{base}"
|
|
729
|
+
display_rebase_warning(workspace_path, wp_id, base_branch, feature_slug)
|
|
730
|
+
else:
|
|
731
|
+
# No explicit base, but workspace is stale (base changed)
|
|
732
|
+
console.print(f"\n[yellow]⚠️ Workspace is stale (base has changed)[/yellow]")
|
|
733
|
+
if vcs_backend == VCSBackend.JUJUTSU:
|
|
734
|
+
console.print("Run [bold]jj workspace update-stale[/bold] to sync")
|
|
735
|
+
else:
|
|
736
|
+
console.print(f"Consider rebasing if needed")
|
|
737
|
+
|
|
738
|
+
# Check for dependent WPs (T079)
|
|
739
|
+
check_for_dependents(repo_root, feature_slug, wp_id)
|
|
740
|
+
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
# Validate workspace path doesn't exist as a non-workspace directory
|
|
744
|
+
if workspace_path.exists():
|
|
745
|
+
console.print(f"[red]Error:[/red] Directory exists but is not a valid workspace")
|
|
746
|
+
console.print(f"Path: {workspace_path}")
|
|
747
|
+
console.print(f"Remove manually: rm -rf {workspace_path}")
|
|
748
|
+
raise typer.Exit(1)
|
|
749
|
+
|
|
750
|
+
# Determine base branch
|
|
751
|
+
if auto_merge_base:
|
|
752
|
+
# Multi-parent: Create merge base combining all dependencies
|
|
753
|
+
merge_result = create_multi_parent_base(
|
|
754
|
+
feature_slug=feature_slug,
|
|
755
|
+
wp_id=wp_id,
|
|
756
|
+
dependencies=declared_deps,
|
|
757
|
+
repo_root=repo_root,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
if not merge_result.success:
|
|
761
|
+
tracker.error("create", "merge base creation failed")
|
|
762
|
+
console.print(tracker.render())
|
|
763
|
+
console.print(f"\n[red]Error:[/red] Failed to create merge base")
|
|
764
|
+
console.print(f"Reason: {merge_result.error}")
|
|
765
|
+
|
|
766
|
+
if merge_result.conflicts:
|
|
767
|
+
console.print(f"\n[yellow]Conflicts in:[/yellow]")
|
|
768
|
+
for conflict_file in merge_result.conflicts:
|
|
769
|
+
console.print(f" - {conflict_file}")
|
|
770
|
+
console.print(f"\n[dim]Resolve conflicts manually, then re-run implement[/dim]")
|
|
771
|
+
|
|
772
|
+
raise typer.Exit(1)
|
|
773
|
+
|
|
774
|
+
# Use merge base branch
|
|
775
|
+
base_branch = merge_result.branch_name
|
|
776
|
+
|
|
777
|
+
elif base is None:
|
|
778
|
+
# No dependencies - branch from primary branch
|
|
779
|
+
base_branch = resolve_primary_branch(repo_root)
|
|
780
|
+
else:
|
|
781
|
+
# Has dependencies - branch from base WP's branch
|
|
782
|
+
base_branch = f"{feature_slug}-{base}"
|
|
783
|
+
|
|
784
|
+
# Validate base branch/workspace exists
|
|
785
|
+
base_workspace_path = repo_root / ".worktrees" / f"{feature_slug}-{base}"
|
|
786
|
+
base_workspace_info = vcs.get_workspace_info(base_workspace_path)
|
|
787
|
+
if base_workspace_info is None:
|
|
788
|
+
tracker.error("create", f"base workspace {base} not found")
|
|
789
|
+
console.print(tracker.render())
|
|
790
|
+
console.print(f"[red]Error:[/red] Base workspace {base} does not exist")
|
|
791
|
+
console.print(f"Implement {base} first: spec-kitty implement {base}")
|
|
792
|
+
raise typer.Exit(1)
|
|
793
|
+
|
|
794
|
+
# Use the base workspace's current branch for git, or the revision for jj
|
|
795
|
+
if vcs_backend == VCSBackend.GIT:
|
|
796
|
+
if base_workspace_info.current_branch:
|
|
797
|
+
base_branch = base_workspace_info.current_branch
|
|
798
|
+
# For git, verify the branch exists
|
|
799
|
+
result = subprocess.run(
|
|
800
|
+
["git", "rev-parse", "--verify", base_branch],
|
|
801
|
+
cwd=repo_root,
|
|
802
|
+
capture_output=True,
|
|
803
|
+
check=False
|
|
804
|
+
)
|
|
805
|
+
if result.returncode != 0:
|
|
806
|
+
tracker.error("create", f"base branch {base_branch} not found")
|
|
807
|
+
console.print(tracker.render())
|
|
808
|
+
console.print(f"[red]Error:[/red] Base branch {base_branch} does not exist")
|
|
809
|
+
raise typer.Exit(1)
|
|
810
|
+
|
|
811
|
+
# Create workspace using VCS abstraction
|
|
812
|
+
# For git: sparse_exclude excludes kitty-specs/ from worktree
|
|
813
|
+
# For jj: no sparse-checkout needed (jj has different isolation model)
|
|
814
|
+
if vcs_backend == VCSBackend.GIT:
|
|
815
|
+
create_result = vcs.create_workspace(
|
|
816
|
+
workspace_path=workspace_path,
|
|
817
|
+
workspace_name=workspace_name,
|
|
818
|
+
base_branch=base_branch,
|
|
819
|
+
repo_root=repo_root,
|
|
820
|
+
sparse_exclude=["kitty-specs/"],
|
|
821
|
+
)
|
|
822
|
+
else:
|
|
823
|
+
# jj workspace creation
|
|
824
|
+
create_result = vcs.create_workspace(
|
|
825
|
+
workspace_path=workspace_path,
|
|
826
|
+
workspace_name=workspace_name,
|
|
827
|
+
base_branch=base_branch,
|
|
828
|
+
repo_root=repo_root,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
if not create_result.success:
|
|
832
|
+
tracker.error("create", "workspace creation failed")
|
|
833
|
+
console.print(tracker.render())
|
|
834
|
+
console.print(f"\n[red]Error:[/red] Failed to create workspace")
|
|
835
|
+
console.print(f"Error: {create_result.error}")
|
|
836
|
+
raise typer.Exit(1)
|
|
837
|
+
|
|
838
|
+
# For git, confirm sparse-checkout was applied
|
|
839
|
+
if vcs_backend == VCSBackend.GIT:
|
|
840
|
+
console.print("[cyan]→ Sparse-checkout configured (kitty-specs/ excluded, agents read from main)[/cyan]")
|
|
841
|
+
|
|
842
|
+
# Step 3.5: Get base commit SHA for tracking
|
|
843
|
+
result = subprocess.run(
|
|
844
|
+
["git", "rev-parse", base_branch],
|
|
845
|
+
cwd=repo_root,
|
|
846
|
+
capture_output=True,
|
|
847
|
+
text=True,
|
|
848
|
+
check=False
|
|
849
|
+
)
|
|
850
|
+
base_commit_sha = result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
851
|
+
|
|
852
|
+
# Step 3.6: Update WP frontmatter with base tracking
|
|
853
|
+
try:
|
|
854
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
855
|
+
|
|
856
|
+
# Update frontmatter with base tracking fields
|
|
857
|
+
update_fields(wp_file, {
|
|
858
|
+
"base_branch": base_branch,
|
|
859
|
+
"base_commit": base_commit_sha,
|
|
860
|
+
"created_at": created_at,
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
console.print(f"[cyan]→ Base tracking: {base_branch} @ {base_commit_sha[:7]}[/cyan]")
|
|
864
|
+
except Exception as e:
|
|
865
|
+
console.print(f"[yellow]Warning:[/yellow] Could not update base tracking in frontmatter: {e}")
|
|
866
|
+
|
|
867
|
+
# Step 3.7: Create workspace context file
|
|
868
|
+
try:
|
|
869
|
+
# Note if this was created via multi-parent merge
|
|
870
|
+
created_by = "implement-command"
|
|
871
|
+
if auto_merge_base:
|
|
872
|
+
created_by = "implement-command-multi-parent-merge"
|
|
873
|
+
|
|
874
|
+
context = WorkspaceContext(
|
|
875
|
+
wp_id=wp_id,
|
|
876
|
+
feature_slug=feature_slug,
|
|
877
|
+
worktree_path=str(workspace_path.relative_to(repo_root)),
|
|
878
|
+
branch_name=branch_name,
|
|
879
|
+
base_branch=base_branch,
|
|
880
|
+
base_commit=base_commit_sha,
|
|
881
|
+
dependencies=declared_deps,
|
|
882
|
+
created_at=created_at,
|
|
883
|
+
created_by=created_by,
|
|
884
|
+
vcs_backend=vcs_backend.value,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
context_path = save_context(repo_root, context)
|
|
888
|
+
console.print(f"[cyan]→ Workspace context: {context_path.relative_to(repo_root)}[/cyan]")
|
|
889
|
+
except Exception as e:
|
|
890
|
+
console.print(f"[yellow]Warning:[/yellow] Could not create workspace context: {e}")
|
|
891
|
+
|
|
892
|
+
tracker.complete("create", f"Workspace: {workspace_path.relative_to(repo_root)}")
|
|
893
|
+
|
|
894
|
+
except typer.Exit:
|
|
895
|
+
console.print(tracker.render())
|
|
896
|
+
raise
|
|
897
|
+
|
|
898
|
+
# Step 4: Update WP lane to "doing" and auto-commit to main
|
|
899
|
+
# This enables multi-agent synchronization - all agents see the claim immediately
|
|
900
|
+
try:
|
|
901
|
+
import os
|
|
902
|
+
|
|
903
|
+
wp = locate_work_package(repo_root, feature_slug, wp_id)
|
|
904
|
+
|
|
905
|
+
# Only update if currently planned (avoid overwriting existing doing/review state)
|
|
906
|
+
current_lane = wp.lane or "planned"
|
|
907
|
+
if current_lane == "planned":
|
|
908
|
+
# Capture current shell PID for audit trail
|
|
909
|
+
shell_pid = str(os.getppid())
|
|
910
|
+
|
|
911
|
+
# Update lane and shell_pid in frontmatter
|
|
912
|
+
updated_front = set_scalar(wp.frontmatter, "lane", "doing")
|
|
913
|
+
updated_front = set_scalar(updated_front, "shell_pid", shell_pid)
|
|
914
|
+
|
|
915
|
+
# Build and write updated document
|
|
916
|
+
updated_doc = build_document(updated_front, wp.body, wp.padding)
|
|
917
|
+
wp.path.write_text(updated_doc, encoding="utf-8")
|
|
918
|
+
|
|
919
|
+
# Auto-commit to main branch
|
|
920
|
+
commit_msg = f"chore: {wp_id} claimed for implementation"
|
|
921
|
+
commit_result = subprocess.run(
|
|
922
|
+
["git", "commit", str(wp.path.resolve()), "-m", commit_msg],
|
|
923
|
+
cwd=repo_root,
|
|
924
|
+
capture_output=True,
|
|
925
|
+
text=True,
|
|
926
|
+
check=False
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
if commit_result.returncode == 0:
|
|
930
|
+
console.print(f"[cyan]→ {wp_id} moved to 'doing' (committed to main)[/cyan]")
|
|
931
|
+
else:
|
|
932
|
+
# Commit failed - file might be unchanged or other issue
|
|
933
|
+
console.print(f"[yellow]Warning:[/yellow] Could not auto-commit lane change")
|
|
934
|
+
if commit_result.stderr:
|
|
935
|
+
console.print(f" {commit_result.stderr.strip()}")
|
|
936
|
+
|
|
937
|
+
except Exception as e:
|
|
938
|
+
# Non-fatal: workspace created but lane update failed
|
|
939
|
+
console.print(f"[yellow]Warning:[/yellow] Could not update WP status: {e}")
|
|
940
|
+
|
|
941
|
+
# Success
|
|
942
|
+
if json_output:
|
|
943
|
+
# JSON output for scripting
|
|
944
|
+
import json
|
|
945
|
+
print(json.dumps({
|
|
946
|
+
"workspace_path": str(workspace_path.relative_to(repo_root)),
|
|
947
|
+
"branch": branch_name,
|
|
948
|
+
"feature": feature_slug,
|
|
949
|
+
"wp_id": wp_id,
|
|
950
|
+
"base": base or "main",
|
|
951
|
+
"status": "created"
|
|
952
|
+
}))
|
|
953
|
+
else:
|
|
954
|
+
# Human-readable output
|
|
955
|
+
console.print(tracker.render())
|
|
956
|
+
console.print(f"\n[bold green]✓ Workspace created successfully[/bold green]")
|
|
957
|
+
|
|
958
|
+
# Check for dependent WPs after creation (T079)
|
|
959
|
+
check_for_dependents(repo_root, feature_slug, wp_id)
|
|
960
|
+
|
|
961
|
+
# CRITICAL: Explicit cd instruction to prevent writing to main
|
|
962
|
+
console.print()
|
|
963
|
+
console.print("[bold yellow]" + "=" * 72 + "[/bold yellow]")
|
|
964
|
+
console.print("[bold yellow]CRITICAL: Change to workspace directory before making any changes![/bold yellow]")
|
|
965
|
+
console.print("[bold yellow]" + "=" * 72 + "[/bold yellow]")
|
|
966
|
+
console.print()
|
|
967
|
+
console.print(f" [bold]cd {workspace_path}[/bold]")
|
|
968
|
+
console.print()
|
|
969
|
+
console.print("[dim]All file edits, writes, and commits MUST happen in this directory.[/dim]")
|
|
970
|
+
console.print("[dim]Writing to main repository instead of the workspace is a critical error.[/dim]")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
__all__ = ["implement"]
|