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,684 @@
|
|
|
1
|
+
"""Fixture data structures for orchestrator e2e testing.
|
|
2
|
+
|
|
3
|
+
This module defines the core data structures for managing test fixtures:
|
|
4
|
+
- FixtureCheckpoint: A restorable snapshot of orchestration state
|
|
5
|
+
- WorktreeMetadata: Information needed to recreate a git worktree
|
|
6
|
+
- TestContext: Complete runtime context for an e2e test
|
|
7
|
+
|
|
8
|
+
It also provides JSON schema validation for:
|
|
9
|
+
- worktrees.json: List of worktree metadata
|
|
10
|
+
- state.json: Serialized OrchestrationRun
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from specify_cli.orchestrator.state import OrchestrationRun
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# Exceptions
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WorktreesFileError(Exception):
|
|
31
|
+
"""Error loading or validating worktrees.json."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StateFileError(Exception):
|
|
37
|
+
"""Error loading or validating state.json."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GitError(Exception):
|
|
43
|
+
"""Error executing git command."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# FixtureCheckpoint Dataclass (T010)
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FixtureCheckpoint:
|
|
55
|
+
"""A restorable snapshot of orchestration state.
|
|
56
|
+
|
|
57
|
+
Represents a checkpoint directory containing:
|
|
58
|
+
- state.json: Serialized OrchestrationRun
|
|
59
|
+
- feature/: Copy of the feature directory
|
|
60
|
+
- worktrees.json: Worktree metadata for recreation
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
"""Checkpoint identifier (e.g., 'wp_created', 'review_pending')."""
|
|
65
|
+
|
|
66
|
+
path: Path
|
|
67
|
+
"""Absolute path to the checkpoint directory."""
|
|
68
|
+
|
|
69
|
+
orchestrator_version: str
|
|
70
|
+
"""Version of spec-kitty that created this checkpoint."""
|
|
71
|
+
|
|
72
|
+
created_at: datetime
|
|
73
|
+
"""When this checkpoint was created."""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def state_file(self) -> Path:
|
|
77
|
+
"""Path to state.json within checkpoint."""
|
|
78
|
+
return self.path / "state.json"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def feature_dir(self) -> Path:
|
|
82
|
+
"""Path to feature/ directory within checkpoint."""
|
|
83
|
+
return self.path / "feature"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def worktrees_file(self) -> Path:
|
|
87
|
+
"""Path to worktrees.json within checkpoint."""
|
|
88
|
+
return self.path / "worktrees.json"
|
|
89
|
+
|
|
90
|
+
def exists(self) -> bool:
|
|
91
|
+
"""Check if all required checkpoint files exist."""
|
|
92
|
+
return (
|
|
93
|
+
self.path.exists()
|
|
94
|
+
and self.state_file.exists()
|
|
95
|
+
and self.feature_dir.exists()
|
|
96
|
+
and self.worktrees_file.exists()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
"""Convert to JSON-serializable dict."""
|
|
101
|
+
return {
|
|
102
|
+
"name": self.name,
|
|
103
|
+
"path": str(self.path),
|
|
104
|
+
"orchestrator_version": self.orchestrator_version,
|
|
105
|
+
"created_at": self.created_at.isoformat(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, data: dict[str, Any]) -> FixtureCheckpoint:
|
|
110
|
+
"""Create from JSON dict."""
|
|
111
|
+
return cls(
|
|
112
|
+
name=data["name"],
|
|
113
|
+
path=Path(data["path"]),
|
|
114
|
+
orchestrator_version=data["orchestrator_version"],
|
|
115
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# WorktreeMetadata Dataclass (T011)
|
|
121
|
+
# =============================================================================
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class WorktreeMetadata:
|
|
126
|
+
"""Information needed to recreate a git worktree.
|
|
127
|
+
|
|
128
|
+
Used in worktrees.json to track which worktrees exist in a fixture
|
|
129
|
+
and how to recreate them when restoring from checkpoint.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
wp_id: str
|
|
133
|
+
"""Work package identifier (e.g., 'WP01')."""
|
|
134
|
+
|
|
135
|
+
branch_name: str
|
|
136
|
+
"""Git branch name for this worktree."""
|
|
137
|
+
|
|
138
|
+
relative_path: str
|
|
139
|
+
"""Path relative to repo root (e.g., '.worktrees/test-feature-WP01')."""
|
|
140
|
+
|
|
141
|
+
commit_hash: str | None = None
|
|
142
|
+
"""Optional commit hash to checkout (None = branch HEAD)."""
|
|
143
|
+
|
|
144
|
+
def to_dict(self) -> dict[str, Any]:
|
|
145
|
+
"""Convert to JSON-serializable dict."""
|
|
146
|
+
return {
|
|
147
|
+
"wp_id": self.wp_id,
|
|
148
|
+
"branch_name": self.branch_name,
|
|
149
|
+
"relative_path": self.relative_path,
|
|
150
|
+
"commit_hash": self.commit_hash,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_dict(cls, data: dict[str, Any]) -> WorktreeMetadata:
|
|
155
|
+
"""Create from JSON dict."""
|
|
156
|
+
return cls(
|
|
157
|
+
wp_id=data["wp_id"],
|
|
158
|
+
branch_name=data["branch_name"],
|
|
159
|
+
relative_path=data["relative_path"],
|
|
160
|
+
commit_hash=data.get("commit_hash"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# =============================================================================
|
|
165
|
+
# TestContext Dataclass (T012)
|
|
166
|
+
# =============================================================================
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class TestContext:
|
|
171
|
+
"""Complete context for running an e2e orchestrator test.
|
|
172
|
+
|
|
173
|
+
Combines:
|
|
174
|
+
- Temporary test environment paths
|
|
175
|
+
- Test path selection (which agents to use)
|
|
176
|
+
- Loaded checkpoint state (if starting from snapshot)
|
|
177
|
+
- Worktree metadata
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
temp_dir: Path
|
|
181
|
+
"""Temporary directory containing the test environment."""
|
|
182
|
+
|
|
183
|
+
repo_root: Path
|
|
184
|
+
"""Root of the test git repository."""
|
|
185
|
+
|
|
186
|
+
feature_dir: Path
|
|
187
|
+
"""Path to the test feature directory."""
|
|
188
|
+
|
|
189
|
+
test_path: Any # TestPath from paths.py - forward reference until WP02 merges
|
|
190
|
+
"""Selected test path with agent assignments."""
|
|
191
|
+
|
|
192
|
+
checkpoint: FixtureCheckpoint | None = None
|
|
193
|
+
"""Loaded checkpoint if test started from snapshot."""
|
|
194
|
+
|
|
195
|
+
orchestration_state: OrchestrationRun | None = None
|
|
196
|
+
"""Loaded state from checkpoint (None if fresh start)."""
|
|
197
|
+
|
|
198
|
+
worktrees: list[WorktreeMetadata] = field(default_factory=list)
|
|
199
|
+
"""Worktree metadata for this test context."""
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def kitty_specs_dir(self) -> Path:
|
|
203
|
+
"""Path to kitty-specs directory in test repo."""
|
|
204
|
+
return self.repo_root / "kitty-specs"
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def worktrees_dir(self) -> Path:
|
|
208
|
+
"""Path to .worktrees directory in test repo."""
|
|
209
|
+
return self.repo_root / ".worktrees"
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def state_file(self) -> Path:
|
|
213
|
+
"""Path to orchestration state file."""
|
|
214
|
+
return self.feature_dir / ".orchestration-state.json"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# =============================================================================
|
|
218
|
+
# worktrees.json Schema Validation (T013)
|
|
219
|
+
# =============================================================================
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_worktrees_file(path: Path) -> list[WorktreeMetadata]:
|
|
223
|
+
"""Load and validate worktrees.json file.
|
|
224
|
+
|
|
225
|
+
Expected format:
|
|
226
|
+
{
|
|
227
|
+
"worktrees": [
|
|
228
|
+
{
|
|
229
|
+
"wp_id": "WP01",
|
|
230
|
+
"branch_name": "test-feature-WP01",
|
|
231
|
+
"relative_path": ".worktrees/test-feature-WP01",
|
|
232
|
+
"commit_hash": null
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
path: Path to worktrees.json
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of WorktreeMetadata objects
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
WorktreesFileError: If file is invalid or missing required fields
|
|
245
|
+
"""
|
|
246
|
+
if not path.exists():
|
|
247
|
+
raise WorktreesFileError(f"Worktrees file not found: {path}")
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
with open(path) as f:
|
|
251
|
+
data = json.load(f)
|
|
252
|
+
except json.JSONDecodeError as e:
|
|
253
|
+
raise WorktreesFileError(f"Invalid JSON in {path}: {e}")
|
|
254
|
+
|
|
255
|
+
# Validate top-level structure
|
|
256
|
+
if not isinstance(data, dict):
|
|
257
|
+
raise WorktreesFileError(f"Expected object, got {type(data).__name__}")
|
|
258
|
+
|
|
259
|
+
if "worktrees" not in data:
|
|
260
|
+
raise WorktreesFileError("Missing 'worktrees' key")
|
|
261
|
+
|
|
262
|
+
worktrees_list = data["worktrees"]
|
|
263
|
+
if not isinstance(worktrees_list, list):
|
|
264
|
+
raise WorktreesFileError("'worktrees' must be an array")
|
|
265
|
+
|
|
266
|
+
# Parse and validate each worktree entry
|
|
267
|
+
result: list[WorktreeMetadata] = []
|
|
268
|
+
required_keys = {"wp_id", "branch_name", "relative_path"}
|
|
269
|
+
|
|
270
|
+
for i, item in enumerate(worktrees_list):
|
|
271
|
+
if not isinstance(item, dict):
|
|
272
|
+
raise WorktreesFileError(f"Worktree entry {i} must be an object")
|
|
273
|
+
|
|
274
|
+
missing = required_keys - set(item.keys())
|
|
275
|
+
if missing:
|
|
276
|
+
raise WorktreesFileError(f"Worktree entry {i} missing required keys: {missing}")
|
|
277
|
+
|
|
278
|
+
result.append(WorktreeMetadata.from_dict(item))
|
|
279
|
+
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def save_worktrees_file(path: Path, worktrees: list[WorktreeMetadata]) -> None:
|
|
284
|
+
"""Save worktrees to JSON file.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
path: Path to write to
|
|
288
|
+
worktrees: List of worktree metadata
|
|
289
|
+
"""
|
|
290
|
+
data = {"worktrees": [w.to_dict() for w in worktrees]}
|
|
291
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
with open(path, "w") as f:
|
|
293
|
+
json.dump(data, f, indent=2)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# =============================================================================
|
|
297
|
+
# state.json Schema Validation (T014)
|
|
298
|
+
# =============================================================================
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def load_state_file(path: Path) -> OrchestrationRun:
|
|
302
|
+
"""Load and validate state.json file.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
path: Path to state.json
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
OrchestrationRun object
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
StateFileError: If file is invalid or cannot be parsed
|
|
312
|
+
"""
|
|
313
|
+
# Import here to avoid circular imports
|
|
314
|
+
from specify_cli.orchestrator.state import OrchestrationRun
|
|
315
|
+
|
|
316
|
+
if not path.exists():
|
|
317
|
+
raise StateFileError(f"State file not found: {path}")
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
with open(path) as f:
|
|
321
|
+
data = json.load(f)
|
|
322
|
+
except json.JSONDecodeError as e:
|
|
323
|
+
raise StateFileError(f"Invalid JSON in {path}: {e}")
|
|
324
|
+
|
|
325
|
+
# Validate required fields per OrchestrationRun schema
|
|
326
|
+
required_fields = {
|
|
327
|
+
"run_id",
|
|
328
|
+
"feature_slug",
|
|
329
|
+
"started_at",
|
|
330
|
+
"status",
|
|
331
|
+
"wps_total",
|
|
332
|
+
"wps_completed",
|
|
333
|
+
"wps_failed",
|
|
334
|
+
"work_packages",
|
|
335
|
+
}
|
|
336
|
+
missing = required_fields - set(data.keys())
|
|
337
|
+
if missing:
|
|
338
|
+
raise StateFileError(f"Missing required fields: {missing}")
|
|
339
|
+
|
|
340
|
+
# Use OrchestrationRun's deserialization
|
|
341
|
+
try:
|
|
342
|
+
return OrchestrationRun.from_dict(data)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
raise StateFileError(f"Failed to parse OrchestrationRun: {e}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def save_state_file(path: Path, state: OrchestrationRun) -> None:
|
|
348
|
+
"""Save OrchestrationRun to JSON file.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
path: Path to write to
|
|
352
|
+
state: Orchestration state
|
|
353
|
+
"""
|
|
354
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
with open(path, "w") as f:
|
|
356
|
+
json.dump(state.to_dict(), f, indent=2)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# =============================================================================
|
|
360
|
+
# Test Helper Functions
|
|
361
|
+
# =============================================================================
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
_cleanup_registry: list[Path] = []
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def register_for_cleanup(path: Path) -> None:
|
|
368
|
+
"""Register a path for cleanup.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
path: Path to register
|
|
372
|
+
"""
|
|
373
|
+
_cleanup_registry.append(path)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def cleanup_temp_dir(path: Path) -> None:
|
|
377
|
+
"""Clean up a temporary directory.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
path: Directory to remove
|
|
381
|
+
"""
|
|
382
|
+
import shutil
|
|
383
|
+
|
|
384
|
+
if path.exists():
|
|
385
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def cleanup_test_context(ctx: TestContext) -> None:
|
|
389
|
+
"""Clean up a test context.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
ctx: Context to clean up
|
|
393
|
+
"""
|
|
394
|
+
cleanup_temp_dir(ctx.temp_dir)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def copy_fixture_to_temp(checkpoint: FixtureCheckpoint) -> Path:
|
|
398
|
+
"""Copy a fixture checkpoint to a temp directory.
|
|
399
|
+
|
|
400
|
+
Creates directory structure:
|
|
401
|
+
temp_dir/
|
|
402
|
+
kitty-specs/test-feature/ (copied from checkpoint.feature_dir)
|
|
403
|
+
.orchestration-state.json (copied from checkpoint.state_file)
|
|
404
|
+
worktrees.json
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
checkpoint: Checkpoint to copy
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Path to temporary directory
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
FileNotFoundError: If checkpoint is incomplete (missing required files)
|
|
414
|
+
"""
|
|
415
|
+
import shutil
|
|
416
|
+
import tempfile
|
|
417
|
+
|
|
418
|
+
# Validate checkpoint has required files
|
|
419
|
+
if not checkpoint.worktrees_file.exists():
|
|
420
|
+
raise FileNotFoundError(
|
|
421
|
+
f"Checkpoint not found or incomplete: missing {checkpoint.worktrees_file}"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
temp_dir = Path(tempfile.mkdtemp(prefix=f"orchestrator_test_{checkpoint.name}_"))
|
|
425
|
+
|
|
426
|
+
# Create kitty-specs/test-feature directory structure
|
|
427
|
+
feature_dest = temp_dir / "kitty-specs" / "test-feature"
|
|
428
|
+
feature_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
|
|
430
|
+
# Copy feature directory if it exists
|
|
431
|
+
if checkpoint.feature_dir.exists():
|
|
432
|
+
shutil.copytree(checkpoint.feature_dir, feature_dest)
|
|
433
|
+
|
|
434
|
+
# Copy state file into the feature directory as .orchestration-state.json
|
|
435
|
+
if checkpoint.state_file.exists():
|
|
436
|
+
shutil.copy(checkpoint.state_file, feature_dest / ".orchestration-state.json")
|
|
437
|
+
|
|
438
|
+
# Copy worktrees file to temp dir root
|
|
439
|
+
shutil.copy(checkpoint.worktrees_file, temp_dir / "worktrees.json")
|
|
440
|
+
|
|
441
|
+
register_for_cleanup(temp_dir)
|
|
442
|
+
return temp_dir
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def init_git_repo(path: Path) -> None:
|
|
446
|
+
"""Initialize a git repository at path with initial commit.
|
|
447
|
+
|
|
448
|
+
Creates a git repo, configures user, and makes an initial commit.
|
|
449
|
+
If no files exist, creates a .gitkeep file.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
path: Directory to initialize
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
GitError: If git command fails
|
|
456
|
+
"""
|
|
457
|
+
import subprocess
|
|
458
|
+
|
|
459
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
subprocess.run(
|
|
463
|
+
["git", "init"],
|
|
464
|
+
cwd=path,
|
|
465
|
+
capture_output=True,
|
|
466
|
+
check=True,
|
|
467
|
+
)
|
|
468
|
+
subprocess.run(
|
|
469
|
+
["git", "config", "user.name", "Test User"],
|
|
470
|
+
cwd=path,
|
|
471
|
+
capture_output=True,
|
|
472
|
+
check=True,
|
|
473
|
+
)
|
|
474
|
+
subprocess.run(
|
|
475
|
+
["git", "config", "user.email", "test@example.com"],
|
|
476
|
+
cwd=path,
|
|
477
|
+
capture_output=True,
|
|
478
|
+
check=True,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Create a .gitkeep if no files exist (to ensure we can make a commit)
|
|
482
|
+
gitkeep = path / ".gitkeep"
|
|
483
|
+
if not gitkeep.exists():
|
|
484
|
+
gitkeep.write_text("")
|
|
485
|
+
|
|
486
|
+
# Add all files and make initial commit
|
|
487
|
+
subprocess.run(
|
|
488
|
+
["git", "add", "."],
|
|
489
|
+
cwd=path,
|
|
490
|
+
capture_output=True,
|
|
491
|
+
check=True,
|
|
492
|
+
)
|
|
493
|
+
subprocess.run(
|
|
494
|
+
["git", "commit", "-m", "Initial test fixture commit"],
|
|
495
|
+
cwd=path,
|
|
496
|
+
capture_output=True,
|
|
497
|
+
check=True,
|
|
498
|
+
)
|
|
499
|
+
except subprocess.CalledProcessError as e:
|
|
500
|
+
raise GitError(f"Failed to init git repo: {e}")
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def create_worktrees_from_metadata(
|
|
504
|
+
repo_root: Path, worktrees: list[WorktreeMetadata]
|
|
505
|
+
) -> None:
|
|
506
|
+
"""Create git worktrees from metadata.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
repo_root: Root of the main repository
|
|
510
|
+
worktrees: List of worktree metadata
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
GitError: If worktree creation fails
|
|
514
|
+
"""
|
|
515
|
+
import subprocess
|
|
516
|
+
|
|
517
|
+
for wt in worktrees:
|
|
518
|
+
# Use relative_path from metadata to determine worktree location
|
|
519
|
+
worktree_path = repo_root / wt.relative_path
|
|
520
|
+
worktree_path.parent.mkdir(parents=True, exist_ok=True)
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
# First create the branch
|
|
524
|
+
subprocess.run(
|
|
525
|
+
["git", "branch", wt.branch_name],
|
|
526
|
+
cwd=repo_root,
|
|
527
|
+
capture_output=True,
|
|
528
|
+
)
|
|
529
|
+
# Then create the worktree
|
|
530
|
+
subprocess.run(
|
|
531
|
+
["git", "worktree", "add", str(worktree_path), wt.branch_name],
|
|
532
|
+
cwd=repo_root,
|
|
533
|
+
capture_output=True,
|
|
534
|
+
check=True,
|
|
535
|
+
)
|
|
536
|
+
except subprocess.CalledProcessError as e:
|
|
537
|
+
raise GitError(f"Failed to create worktree for {wt.wp_id}: {e}")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def load_orchestration_state(path: Path) -> "OrchestrationRun":
|
|
541
|
+
"""Load orchestration state from a feature directory or state file.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
path: Path to feature directory or state.json file.
|
|
545
|
+
If a directory, looks for .orchestration-state.json inside.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
OrchestrationRun instance
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
StateFileError: If loading fails or state file not found
|
|
552
|
+
"""
|
|
553
|
+
if path.is_dir():
|
|
554
|
+
# Assume it's a feature directory, look for state file inside
|
|
555
|
+
state_file = path / ".orchestration-state.json"
|
|
556
|
+
else:
|
|
557
|
+
state_file = path
|
|
558
|
+
|
|
559
|
+
return load_state_file(state_file)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def load_checkpoint(
|
|
563
|
+
checkpoint: FixtureCheckpoint,
|
|
564
|
+
test_path: Any | None = None,
|
|
565
|
+
) -> TestContext:
|
|
566
|
+
"""Load a checkpoint into a TestContext.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
checkpoint: Checkpoint to load from
|
|
570
|
+
test_path: Optional TestPath to use (creates mock if None)
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
TestContext with checkpoint loaded
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
FileNotFoundError: If checkpoint files are missing
|
|
577
|
+
StateFileError: If state.json is invalid
|
|
578
|
+
"""
|
|
579
|
+
import shutil
|
|
580
|
+
import tempfile
|
|
581
|
+
|
|
582
|
+
# Validate checkpoint has required files
|
|
583
|
+
if not checkpoint.feature_dir.exists():
|
|
584
|
+
raise FileNotFoundError(f"Checkpoint feature directory missing: {checkpoint.feature_dir}")
|
|
585
|
+
|
|
586
|
+
# Create temp directory for test
|
|
587
|
+
temp_dir = Path(tempfile.mkdtemp(prefix=f"orchestrator_test_{checkpoint.name}_"))
|
|
588
|
+
register_for_cleanup(temp_dir)
|
|
589
|
+
|
|
590
|
+
# Copy files to temp_dir
|
|
591
|
+
state_file = temp_dir / "state.json"
|
|
592
|
+
feature_dir = temp_dir / "feature"
|
|
593
|
+
repo_root = temp_dir / "repo"
|
|
594
|
+
|
|
595
|
+
if checkpoint.state_file.exists():
|
|
596
|
+
shutil.copy(checkpoint.state_file, state_file)
|
|
597
|
+
|
|
598
|
+
if checkpoint.feature_dir.exists():
|
|
599
|
+
shutil.copytree(checkpoint.feature_dir, feature_dir)
|
|
600
|
+
|
|
601
|
+
# Also copy state.json to where TestContext.state_file property expects it
|
|
602
|
+
expected_state_file = feature_dir / ".orchestration-state.json"
|
|
603
|
+
if checkpoint.state_file.exists():
|
|
604
|
+
shutil.copy(checkpoint.state_file, expected_state_file)
|
|
605
|
+
|
|
606
|
+
# Initialize git repo
|
|
607
|
+
init_git_repo(repo_root)
|
|
608
|
+
|
|
609
|
+
# Load worktrees metadata if present
|
|
610
|
+
worktrees: list[WorktreeMetadata] = []
|
|
611
|
+
if checkpoint.worktrees_file.exists():
|
|
612
|
+
worktrees = load_worktrees_file(checkpoint.worktrees_file)
|
|
613
|
+
# Create the worktrees in the repo
|
|
614
|
+
if worktrees:
|
|
615
|
+
create_worktrees_from_metadata(repo_root, worktrees)
|
|
616
|
+
|
|
617
|
+
# Load state if it exists
|
|
618
|
+
orchestration_state = None
|
|
619
|
+
if state_file.exists():
|
|
620
|
+
orchestration_state = load_state_file(state_file)
|
|
621
|
+
|
|
622
|
+
# Use provided test_path or create a mock one
|
|
623
|
+
if test_path is None:
|
|
624
|
+
from specify_cli.orchestrator.testing.paths import TestPath
|
|
625
|
+
|
|
626
|
+
test_path = TestPath(
|
|
627
|
+
path_type="1-agent",
|
|
628
|
+
implementation_agent="mock",
|
|
629
|
+
review_agent="mock",
|
|
630
|
+
available_agents=["mock"],
|
|
631
|
+
fallback_agent=None,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
return TestContext(
|
|
635
|
+
temp_dir=temp_dir,
|
|
636
|
+
repo_root=repo_root,
|
|
637
|
+
feature_dir=feature_dir,
|
|
638
|
+
test_path=test_path,
|
|
639
|
+
checkpoint=checkpoint,
|
|
640
|
+
orchestration_state=orchestration_state,
|
|
641
|
+
worktrees=worktrees,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def setup_test_repo(tmp_path: Path, feature_slug: str = "test-feature") -> TestContext:
|
|
646
|
+
"""Set up a test repository with basic structure.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
tmp_path: pytest tmp_path
|
|
650
|
+
feature_slug: Name for the test feature
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
TestContext for the test
|
|
654
|
+
"""
|
|
655
|
+
repo_root = tmp_path / "repo"
|
|
656
|
+
init_git_repo(repo_root)
|
|
657
|
+
|
|
658
|
+
# Create feature directory
|
|
659
|
+
feature_dir = repo_root / "kitty-specs" / feature_slug
|
|
660
|
+
feature_dir.mkdir(parents=True)
|
|
661
|
+
tasks_dir = feature_dir / "tasks"
|
|
662
|
+
tasks_dir.mkdir()
|
|
663
|
+
|
|
664
|
+
# Create basic files
|
|
665
|
+
(feature_dir / "spec.md").write_text("# Test Spec\n")
|
|
666
|
+
(feature_dir / "plan.md").write_text("# Test Plan\n")
|
|
667
|
+
|
|
668
|
+
# Create a mock test_path
|
|
669
|
+
from specify_cli.orchestrator.testing.paths import TestPath
|
|
670
|
+
|
|
671
|
+
mock_path = TestPath(
|
|
672
|
+
path_type="1-agent",
|
|
673
|
+
implementation_agent="mock",
|
|
674
|
+
review_agent="mock",
|
|
675
|
+
available_agents=["mock"],
|
|
676
|
+
fallback_agent=None,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
return TestContext(
|
|
680
|
+
temp_dir=tmp_path,
|
|
681
|
+
repo_root=repo_root,
|
|
682
|
+
feature_dir=feature_dir,
|
|
683
|
+
test_path=mock_path,
|
|
684
|
+
)
|