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.
Files changed (242) hide show
  1. spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
  2. spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
  3. spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
  4. spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
  5. spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
  6. specify_cli/__init__.py +171 -0
  7. specify_cli/acceptance.py +627 -0
  8. specify_cli/agent_utils/README.md +157 -0
  9. specify_cli/agent_utils/__init__.py +9 -0
  10. specify_cli/agent_utils/status.py +356 -0
  11. specify_cli/cli/__init__.py +6 -0
  12. specify_cli/cli/commands/__init__.py +46 -0
  13. specify_cli/cli/commands/accept.py +189 -0
  14. specify_cli/cli/commands/agent/__init__.py +22 -0
  15. specify_cli/cli/commands/agent/config.py +382 -0
  16. specify_cli/cli/commands/agent/context.py +191 -0
  17. specify_cli/cli/commands/agent/feature.py +1057 -0
  18. specify_cli/cli/commands/agent/release.py +11 -0
  19. specify_cli/cli/commands/agent/tasks.py +1253 -0
  20. specify_cli/cli/commands/agent/workflow.py +801 -0
  21. specify_cli/cli/commands/context.py +246 -0
  22. specify_cli/cli/commands/dashboard.py +85 -0
  23. specify_cli/cli/commands/implement.py +973 -0
  24. specify_cli/cli/commands/init.py +827 -0
  25. specify_cli/cli/commands/init_help.py +62 -0
  26. specify_cli/cli/commands/merge.py +755 -0
  27. specify_cli/cli/commands/mission.py +240 -0
  28. specify_cli/cli/commands/ops.py +265 -0
  29. specify_cli/cli/commands/orchestrate.py +640 -0
  30. specify_cli/cli/commands/repair.py +175 -0
  31. specify_cli/cli/commands/research.py +165 -0
  32. specify_cli/cli/commands/sync.py +364 -0
  33. specify_cli/cli/commands/upgrade.py +249 -0
  34. specify_cli/cli/commands/validate_encoding.py +186 -0
  35. specify_cli/cli/commands/validate_tasks.py +186 -0
  36. specify_cli/cli/commands/verify.py +310 -0
  37. specify_cli/cli/helpers.py +123 -0
  38. specify_cli/cli/step_tracker.py +91 -0
  39. specify_cli/cli/ui.py +192 -0
  40. specify_cli/core/__init__.py +53 -0
  41. specify_cli/core/agent_context.py +311 -0
  42. specify_cli/core/config.py +96 -0
  43. specify_cli/core/context_validation.py +362 -0
  44. specify_cli/core/dependency_graph.py +351 -0
  45. specify_cli/core/git_ops.py +129 -0
  46. specify_cli/core/multi_parent_merge.py +323 -0
  47. specify_cli/core/paths.py +260 -0
  48. specify_cli/core/project_resolver.py +110 -0
  49. specify_cli/core/stale_detection.py +263 -0
  50. specify_cli/core/tool_checker.py +79 -0
  51. specify_cli/core/utils.py +43 -0
  52. specify_cli/core/vcs/__init__.py +114 -0
  53. specify_cli/core/vcs/detection.py +341 -0
  54. specify_cli/core/vcs/exceptions.py +85 -0
  55. specify_cli/core/vcs/git.py +1304 -0
  56. specify_cli/core/vcs/jujutsu.py +1208 -0
  57. specify_cli/core/vcs/protocol.py +285 -0
  58. specify_cli/core/vcs/types.py +249 -0
  59. specify_cli/core/version_checker.py +261 -0
  60. specify_cli/core/worktree.py +506 -0
  61. specify_cli/dashboard/__init__.py +28 -0
  62. specify_cli/dashboard/diagnostics.py +204 -0
  63. specify_cli/dashboard/handlers/__init__.py +17 -0
  64. specify_cli/dashboard/handlers/api.py +143 -0
  65. specify_cli/dashboard/handlers/base.py +65 -0
  66. specify_cli/dashboard/handlers/features.py +390 -0
  67. specify_cli/dashboard/handlers/router.py +81 -0
  68. specify_cli/dashboard/handlers/static.py +50 -0
  69. specify_cli/dashboard/lifecycle.py +541 -0
  70. specify_cli/dashboard/scanner.py +437 -0
  71. specify_cli/dashboard/server.py +123 -0
  72. specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
  73. specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
  74. specify_cli/dashboard/static/spec-kitty.png +0 -0
  75. specify_cli/dashboard/templates/__init__.py +36 -0
  76. specify_cli/dashboard/templates/index.html +258 -0
  77. specify_cli/doc_generators.py +621 -0
  78. specify_cli/doc_state.py +408 -0
  79. specify_cli/frontmatter.py +384 -0
  80. specify_cli/gap_analysis.py +915 -0
  81. specify_cli/gitignore_manager.py +300 -0
  82. specify_cli/guards.py +145 -0
  83. specify_cli/legacy_detector.py +83 -0
  84. specify_cli/manifest.py +286 -0
  85. specify_cli/merge/__init__.py +63 -0
  86. specify_cli/merge/executor.py +653 -0
  87. specify_cli/merge/forecast.py +215 -0
  88. specify_cli/merge/ordering.py +126 -0
  89. specify_cli/merge/preflight.py +230 -0
  90. specify_cli/merge/state.py +185 -0
  91. specify_cli/merge/status_resolver.py +354 -0
  92. specify_cli/mission.py +654 -0
  93. specify_cli/missions/documentation/command-templates/implement.md +309 -0
  94. specify_cli/missions/documentation/command-templates/plan.md +275 -0
  95. specify_cli/missions/documentation/command-templates/review.md +344 -0
  96. specify_cli/missions/documentation/command-templates/specify.md +206 -0
  97. specify_cli/missions/documentation/command-templates/tasks.md +189 -0
  98. specify_cli/missions/documentation/mission.yaml +113 -0
  99. specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
  100. specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
  101. specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
  102. specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
  103. specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
  104. specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
  105. specify_cli/missions/documentation/templates/plan-template.md +269 -0
  106. specify_cli/missions/documentation/templates/release-template.md +222 -0
  107. specify_cli/missions/documentation/templates/spec-template.md +172 -0
  108. specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
  109. specify_cli/missions/documentation/templates/tasks-template.md +159 -0
  110. specify_cli/missions/research/command-templates/merge.md +388 -0
  111. specify_cli/missions/research/command-templates/plan.md +125 -0
  112. specify_cli/missions/research/command-templates/review.md +144 -0
  113. specify_cli/missions/research/command-templates/tasks.md +225 -0
  114. specify_cli/missions/research/mission.yaml +115 -0
  115. specify_cli/missions/research/templates/data-model-template.md +33 -0
  116. specify_cli/missions/research/templates/plan-template.md +161 -0
  117. specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
  118. specify_cli/missions/research/templates/research/source-register.csv +18 -0
  119. specify_cli/missions/research/templates/research-template.md +35 -0
  120. specify_cli/missions/research/templates/spec-template.md +64 -0
  121. specify_cli/missions/research/templates/task-prompt-template.md +148 -0
  122. specify_cli/missions/research/templates/tasks-template.md +114 -0
  123. specify_cli/missions/software-dev/command-templates/accept.md +75 -0
  124. specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
  125. specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
  126. specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
  127. specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
  128. specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
  129. specify_cli/missions/software-dev/command-templates/implement.md +41 -0
  130. specify_cli/missions/software-dev/command-templates/merge.md +383 -0
  131. specify_cli/missions/software-dev/command-templates/plan.md +171 -0
  132. specify_cli/missions/software-dev/command-templates/review.md +32 -0
  133. specify_cli/missions/software-dev/command-templates/specify.md +321 -0
  134. specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
  135. specify_cli/missions/software-dev/mission.yaml +100 -0
  136. specify_cli/missions/software-dev/templates/plan-template.md +132 -0
  137. specify_cli/missions/software-dev/templates/spec-template.md +116 -0
  138. specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
  139. specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
  140. specify_cli/orchestrator/__init__.py +75 -0
  141. specify_cli/orchestrator/agent_config.py +224 -0
  142. specify_cli/orchestrator/agents/__init__.py +170 -0
  143. specify_cli/orchestrator/agents/augment.py +112 -0
  144. specify_cli/orchestrator/agents/base.py +243 -0
  145. specify_cli/orchestrator/agents/claude.py +112 -0
  146. specify_cli/orchestrator/agents/codex.py +106 -0
  147. specify_cli/orchestrator/agents/copilot.py +137 -0
  148. specify_cli/orchestrator/agents/cursor.py +139 -0
  149. specify_cli/orchestrator/agents/gemini.py +115 -0
  150. specify_cli/orchestrator/agents/kilocode.py +94 -0
  151. specify_cli/orchestrator/agents/opencode.py +132 -0
  152. specify_cli/orchestrator/agents/qwen.py +96 -0
  153. specify_cli/orchestrator/config.py +455 -0
  154. specify_cli/orchestrator/executor.py +642 -0
  155. specify_cli/orchestrator/integration.py +1230 -0
  156. specify_cli/orchestrator/monitor.py +898 -0
  157. specify_cli/orchestrator/scheduler.py +832 -0
  158. specify_cli/orchestrator/state.py +508 -0
  159. specify_cli/orchestrator/testing/__init__.py +122 -0
  160. specify_cli/orchestrator/testing/availability.py +346 -0
  161. specify_cli/orchestrator/testing/fixtures.py +684 -0
  162. specify_cli/orchestrator/testing/paths.py +218 -0
  163. specify_cli/plan_validation.py +107 -0
  164. specify_cli/scripts/debug-dashboard-scan.py +61 -0
  165. specify_cli/scripts/tasks/acceptance_support.py +695 -0
  166. specify_cli/scripts/tasks/task_helpers.py +506 -0
  167. specify_cli/scripts/tasks/tasks_cli.py +848 -0
  168. specify_cli/scripts/validate_encoding.py +180 -0
  169. specify_cli/task_metadata_validation.py +274 -0
  170. specify_cli/tasks_support.py +447 -0
  171. specify_cli/template/__init__.py +47 -0
  172. specify_cli/template/asset_generator.py +206 -0
  173. specify_cli/template/github_client.py +334 -0
  174. specify_cli/template/manager.py +193 -0
  175. specify_cli/template/renderer.py +99 -0
  176. specify_cli/templates/AGENTS.md +190 -0
  177. specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
  178. specify_cli/templates/agent-file-template.md +35 -0
  179. specify_cli/templates/checklist-template.md +42 -0
  180. specify_cli/templates/claudeignore-template +58 -0
  181. specify_cli/templates/command-templates/accept.md +141 -0
  182. specify_cli/templates/command-templates/analyze.md +253 -0
  183. specify_cli/templates/command-templates/checklist.md +352 -0
  184. specify_cli/templates/command-templates/clarify.md +224 -0
  185. specify_cli/templates/command-templates/constitution.md +432 -0
  186. specify_cli/templates/command-templates/dashboard.md +175 -0
  187. specify_cli/templates/command-templates/implement.md +190 -0
  188. specify_cli/templates/command-templates/merge.md +374 -0
  189. specify_cli/templates/command-templates/plan.md +171 -0
  190. specify_cli/templates/command-templates/research.md +88 -0
  191. specify_cli/templates/command-templates/review.md +510 -0
  192. specify_cli/templates/command-templates/specify.md +321 -0
  193. specify_cli/templates/command-templates/status.md +92 -0
  194. specify_cli/templates/command-templates/tasks.md +199 -0
  195. specify_cli/templates/git-hooks/pre-commit +22 -0
  196. specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
  197. specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
  198. specify_cli/templates/plan-template.md +108 -0
  199. specify_cli/templates/spec-template.md +118 -0
  200. specify_cli/templates/task-prompt-template.md +165 -0
  201. specify_cli/templates/tasks-template.md +161 -0
  202. specify_cli/templates/vscode-settings.json +13 -0
  203. specify_cli/text_sanitization.py +225 -0
  204. specify_cli/upgrade/__init__.py +18 -0
  205. specify_cli/upgrade/detector.py +239 -0
  206. specify_cli/upgrade/metadata.py +182 -0
  207. specify_cli/upgrade/migrations/__init__.py +65 -0
  208. specify_cli/upgrade/migrations/base.py +80 -0
  209. specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
  210. specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
  211. specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
  212. specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
  213. specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
  214. specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
  215. specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
  216. specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
  217. specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
  218. specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
  219. specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
  220. specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
  221. specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
  222. specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
  223. specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
  224. specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
  225. specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
  226. specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
  227. specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
  228. specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
  229. specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
  230. specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
  231. specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
  232. specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
  233. specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
  234. specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
  235. specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
  236. specify_cli/upgrade/registry.py +121 -0
  237. specify_cli/upgrade/runner.py +284 -0
  238. specify_cli/validators/__init__.py +14 -0
  239. specify_cli/validators/paths.py +154 -0
  240. specify_cli/validators/research.py +428 -0
  241. specify_cli/verify_enhanced.py +270 -0
  242. specify_cli/workspace_context.py +224 -0
specify_cli/mission.py ADDED
@@ -0,0 +1,654 @@
1
+ """Mission system for Spec Kitty.
2
+
3
+ This module provides the infrastructure for loading and managing missions,
4
+ which allow Spec Kitty to support multiple domains (software dev, research,
5
+ writing, etc.) with domain-specific templates, workflows, and validation.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import warnings
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Literal, Optional, Tuple
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
16
+
17
+
18
+ class MissionError(Exception):
19
+ """Base exception for mission-related errors."""
20
+ pass
21
+
22
+
23
+ class MissionNotFoundError(MissionError):
24
+ """Raised when a mission cannot be found."""
25
+ pass
26
+
27
+
28
+ MISSION_ROOT_FIELDS: tuple[str, ...] = (
29
+ "name",
30
+ "description",
31
+ "version",
32
+ "domain",
33
+ "workflow",
34
+ "artifacts",
35
+ "paths",
36
+ "validation",
37
+ "mcp_tools",
38
+ "agent_context",
39
+ "task_metadata",
40
+ "commands",
41
+ )
42
+
43
+
44
+ class PhaseConfig(BaseModel):
45
+ """Workflow phase definition."""
46
+
47
+ model_config = ConfigDict(extra="forbid")
48
+
49
+ name: str = Field(..., description="Phase identifier")
50
+ description: str = Field(..., description="Phase description")
51
+
52
+
53
+ class ArtifactsConfig(BaseModel):
54
+ """Required and optional artifacts."""
55
+
56
+ model_config = ConfigDict(extra="forbid")
57
+
58
+ required: List[str] = Field(default_factory=list, description="Artifacts required for acceptance")
59
+ optional: List[str] = Field(default_factory=list, description="Optional artifacts and directories")
60
+
61
+
62
+ class ValidationConfig(BaseModel):
63
+ """Validation rules for the mission."""
64
+
65
+ model_config = ConfigDict(extra="forbid")
66
+
67
+ checks: List[str] = Field(default_factory=list, description="Validation checks executed for this mission")
68
+ custom_validators: bool = Field(default=False, description="Whether validators.py should be invoked")
69
+
70
+
71
+ class WorkflowConfig(BaseModel):
72
+ """Mission workflow configuration."""
73
+
74
+ model_config = ConfigDict(extra="forbid")
75
+
76
+ phases: List[PhaseConfig] = Field(..., min_length=1, description="Ordered workflow phases")
77
+
78
+
79
+ class MCPToolsConfig(BaseModel):
80
+ """Mission MCP tool recommendations."""
81
+
82
+ model_config = ConfigDict(extra="forbid")
83
+
84
+ required: List[str] = Field(default_factory=list)
85
+ recommended: List[str] = Field(default_factory=list)
86
+ optional: List[str] = Field(default_factory=list)
87
+
88
+
89
+ class CommandConfig(BaseModel):
90
+ """Command customization for a mission."""
91
+
92
+ model_config = ConfigDict(extra="forbid")
93
+
94
+ prompt: str = Field(..., description="Command-specific prompt/description")
95
+
96
+
97
+ class TaskMetadataConfig(BaseModel):
98
+ """Task metadata definitions."""
99
+
100
+ model_config = ConfigDict(extra="forbid")
101
+
102
+ required: List[str] = Field(default_factory=list)
103
+ optional: List[str] = Field(default_factory=list)
104
+
105
+
106
+ class MissionConfig(BaseModel):
107
+ """Complete mission configuration schema."""
108
+
109
+ model_config = ConfigDict(extra="forbid")
110
+
111
+ name: str = Field(..., description="Mission display name")
112
+ description: str = Field(..., description="Mission description")
113
+ version: str = Field(..., pattern=r"^\d+\.\d+\.\d+$", description="Semver version (major.minor.patch)")
114
+ domain: Literal["software", "research", "writing", "seo", "other"] = Field(
115
+ ..., description="Mission domain classification"
116
+ )
117
+ workflow: WorkflowConfig = Field(..., description="Workflow definition")
118
+ artifacts: ArtifactsConfig = Field(..., description="Artifacts required/optional")
119
+ paths: Dict[str, str] = Field(
120
+ default_factory=dict,
121
+ description="Path conventions (workspace/tests/deliverables/documentation/data/etc.)",
122
+ )
123
+ validation: ValidationConfig = Field(default_factory=ValidationConfig, description="Validation settings")
124
+ mcp_tools: Optional[MCPToolsConfig] = Field(default=None, description="MCP tool recommendations")
125
+ agent_context: Optional[str] = Field(default=None, description="Agent instructions/personality")
126
+ task_metadata: Optional[TaskMetadataConfig] = Field(default=None, description="Task metadata definitions")
127
+ commands: Optional[Dict[str, CommandConfig]] = Field(default=None, description="Command-specific prompts")
128
+
129
+ def model_post_init(self, __context: Any) -> None: # pragma: no cover - simple warning logic
130
+ """Warn on unknown path convention keys while permitting customization."""
131
+ valid_path_keys = {"workspace", "tests", "deliverables", "documentation", "data"}
132
+ unknown_paths = set(self.paths.keys()) - valid_path_keys
133
+ if unknown_paths:
134
+ warnings.warn(
135
+ f"Unknown path conventions: {sorted(unknown_paths)}. "
136
+ f"Known conventions: {sorted(valid_path_keys)}",
137
+ stacklevel=2,
138
+ )
139
+
140
+
141
+ def _format_validation_error(config_path: Path, error: ValidationError) -> str:
142
+ """Return a human-friendly validation error message."""
143
+ header = [
144
+ f"Invalid mission configuration in {config_path}:",
145
+ "",
146
+ "Detected issues:",
147
+ ]
148
+ for err in error.errors():
149
+ path = " -> ".join(str(part) for part in err.get("loc", ())) or "<root>"
150
+ message = err.get("msg", "Invalid value")
151
+ detail = f"- {path}: {message}"
152
+ if err.get("type") == "extra_forbidden" and len(err.get("loc", ())) == 1:
153
+ valid_fields = ", ".join(MISSION_ROOT_FIELDS)
154
+ detail += f" (check for typos; valid root fields: {valid_fields})"
155
+ header.append(detail)
156
+ header.append("")
157
+ header.append("Refer to kitty-specs/005-refactor-mission-system/data-model.md for the schema definition.")
158
+ return "\n".join(header)
159
+
160
+
161
+ class Mission:
162
+ """Represents a Spec Kitty mission with its configuration and resources."""
163
+
164
+ def __init__(self, mission_path: Path):
165
+ """Initialize a mission from a directory path.
166
+
167
+ Args:
168
+ mission_path: Path to the mission directory containing mission.yaml
169
+
170
+ Raises:
171
+ MissionNotFoundError: If mission directory or config doesn't exist
172
+ """
173
+ self.path = mission_path.resolve()
174
+
175
+ if not self.path.exists():
176
+ raise MissionNotFoundError(f"Mission directory not found: {self.path}")
177
+
178
+ self.config: MissionConfig = self._load_and_validate_config()
179
+
180
+ def _load_and_validate_config(self) -> MissionConfig:
181
+ """Load and validate mission configuration from mission.yaml.
182
+
183
+ Returns:
184
+ MissionConfig instance containing validated configuration
185
+
186
+ Raises:
187
+ MissionNotFoundError: If mission.yaml doesn't exist
188
+ MissionError: If YAML is malformed or validation fails
189
+ yaml.YAMLError: If mission.yaml is malformed
190
+ """
191
+ config_file = self.path / "mission.yaml"
192
+
193
+ if not config_file.exists():
194
+ raise MissionNotFoundError(
195
+ f"Mission config not found: {config_file}\n"
196
+ f"Expected mission.yaml in mission directory"
197
+ )
198
+
199
+ with open(config_file, 'r') as f:
200
+ try:
201
+ raw_config = yaml.safe_load(f) or {}
202
+ except yaml.YAMLError as e:
203
+ raise MissionError(f"Invalid mission.yaml: {e}")
204
+
205
+ if not isinstance(raw_config, dict):
206
+ raise MissionError(
207
+ f"Mission config must be a mapping/dictionary in {config_file}, "
208
+ f"got {type(raw_config).__name__} instead."
209
+ )
210
+
211
+ try:
212
+ return MissionConfig.model_validate(raw_config)
213
+ except ValidationError as error:
214
+ raise MissionError(_format_validation_error(config_file, error)) from error
215
+
216
+ @property
217
+ def name(self) -> str:
218
+ """Get the mission name (e.g., 'Software Dev Kitty')."""
219
+ return self.config.name
220
+
221
+ @property
222
+ def description(self) -> str:
223
+ """Get the mission description."""
224
+ return self.config.description
225
+
226
+ @property
227
+ def version(self) -> str:
228
+ """Get the mission version."""
229
+ return self.config.version
230
+
231
+ @property
232
+ def domain(self) -> str:
233
+ """Get the mission domain (e.g., 'software', 'research')."""
234
+ return self.config.domain
235
+
236
+ @property
237
+ def templates_dir(self) -> Path:
238
+ """Get the templates directory for this mission."""
239
+ return self.path / "templates"
240
+
241
+ @property
242
+ def command_templates_dir(self) -> Path:
243
+ """Get the command templates directory for this mission."""
244
+ return self.path / "command-templates"
245
+
246
+ def get_template(self, template_name: str) -> Path:
247
+ """Get path to a template file.
248
+
249
+ Args:
250
+ template_name: Name of template (e.g., 'spec-template.md')
251
+
252
+ Returns:
253
+ Path to the template file
254
+
255
+ Raises:
256
+ FileNotFoundError: If template doesn't exist
257
+ """
258
+ template_path = self.templates_dir / template_name
259
+
260
+ if not template_path.exists():
261
+ raise FileNotFoundError(
262
+ f"Template not found: {template_path}\n"
263
+ f"Mission: {self.name}\n"
264
+ f"Available templates: {self.list_templates()}"
265
+ )
266
+
267
+ return template_path
268
+
269
+ def get_command_template(self, command_name: str) -> Path:
270
+ """Get path to a command template file.
271
+
272
+ Args:
273
+ command_name: Name of command (e.g., 'plan', 'implement')
274
+
275
+ Returns:
276
+ Path to the command template file
277
+
278
+ Raises:
279
+ FileNotFoundError: If command template doesn't exist
280
+ """
281
+ # Support both with and without .md extension
282
+ if not command_name.endswith('.md'):
283
+ command_name = f"{command_name}.md"
284
+
285
+ command_path = self.command_templates_dir / command_name
286
+
287
+ if not command_path.exists():
288
+ raise FileNotFoundError(
289
+ f"Command template not found: {command_path}\n"
290
+ f"Mission: {self.name}\n"
291
+ f"Available commands: {self.list_commands()}"
292
+ )
293
+
294
+ return command_path
295
+
296
+ def list_templates(self) -> List[str]:
297
+ """List all available templates in this mission."""
298
+ if not self.templates_dir.exists():
299
+ return []
300
+ return [f.name for f in self.templates_dir.glob("*.md")]
301
+
302
+ def list_commands(self) -> List[str]:
303
+ """List all available command templates in this mission."""
304
+ if not self.command_templates_dir.exists():
305
+ return []
306
+ return [f.stem for f in self.command_templates_dir.glob("*.md")]
307
+
308
+ def get_validation_checks(self) -> List[str]:
309
+ """Get list of validation checks for this mission."""
310
+ return list(self.config.validation.checks)
311
+
312
+ def has_custom_validators(self) -> bool:
313
+ """Check if mission has custom validators.py."""
314
+ return self.config.validation.custom_validators
315
+
316
+ def get_workflow_phases(self) -> List[Dict[str, str]]:
317
+ """Get workflow phases for this mission.
318
+
319
+ Returns:
320
+ List of dicts with 'name' and 'description' keys
321
+ """
322
+ return [phase.model_dump() for phase in self.config.workflow.phases]
323
+
324
+ def get_required_artifacts(self) -> List[str]:
325
+ """Get list of required artifacts for this mission."""
326
+ return list(self.config.artifacts.required)
327
+
328
+ def get_optional_artifacts(self) -> List[str]:
329
+ """Get list of optional artifacts for this mission."""
330
+ return list(self.config.artifacts.optional)
331
+
332
+ def get_path_conventions(self) -> Dict[str, str]:
333
+ """Get path conventions for this mission (e.g., workspace, tests)."""
334
+ return dict(self.config.paths)
335
+
336
+ def get_mcp_tools(self) -> Dict[str, List[str]]:
337
+ """Get MCP tools configuration for this mission.
338
+
339
+ Returns:
340
+ Dict with 'required', 'recommended', 'optional' lists
341
+ """
342
+ mcp_tools = self.config.mcp_tools
343
+ if mcp_tools is None:
344
+ return {"required": [], "recommended": [], "optional": []}
345
+ return {
346
+ "required": list(mcp_tools.required),
347
+ "recommended": list(mcp_tools.recommended),
348
+ "optional": list(mcp_tools.optional),
349
+ }
350
+
351
+ def get_agent_context(self) -> str:
352
+ """Get agent personality/instructions for this mission."""
353
+ return self.config.agent_context or ""
354
+
355
+ def get_command_config(self, command_name: str) -> Dict[str, str]:
356
+ """Get configuration for a specific command.
357
+
358
+ Args:
359
+ command_name: Name of command (e.g., 'plan', 'implement')
360
+
361
+ Returns:
362
+ Dict with command configuration (e.g., 'prompt')
363
+ """
364
+ if not self.config.commands:
365
+ return {}
366
+
367
+ command = self.config.commands.get(command_name)
368
+ return command.model_dump() if command else {}
369
+
370
+ def __repr__(self) -> str:
371
+ return f"Mission(name='{self.name}', domain='{self.domain}', version='{self.version}')"
372
+
373
+
374
+ def get_active_mission(project_root: Optional[Path] = None) -> Mission:
375
+ """Get the currently active mission for a project.
376
+
377
+ Args:
378
+ project_root: Path to project root (defaults to current directory)
379
+
380
+ Returns:
381
+ Mission object for the active mission
382
+
383
+ Raises:
384
+ MissionNotFoundError: If no active mission is configured
385
+ """
386
+ if project_root is None:
387
+ project_root = Path.cwd()
388
+
389
+ kittify_dir = project_root / ".kittify"
390
+
391
+ if not kittify_dir.exists():
392
+ raise MissionNotFoundError(
393
+ f"No .kittify directory found in {project_root}\n"
394
+ f"Is this a Spec Kitty project? Run 'spec-kitty init' to create one."
395
+ )
396
+
397
+ # Check for active-mission symlink
398
+ active_mission_link = kittify_dir / "active-mission"
399
+
400
+ if active_mission_link.exists():
401
+ mission_path: Optional[Path] = None
402
+ if active_mission_link.is_symlink():
403
+ # Resolve symlink to actual mission directory (supports relative targets)
404
+ mission_path = active_mission_link.resolve()
405
+ elif active_mission_link.is_file():
406
+ try:
407
+ mission_name = active_mission_link.read_text(encoding="utf-8-sig").strip()
408
+ except OSError:
409
+ mission_name = ""
410
+ if mission_name:
411
+ mission_path = kittify_dir / "missions" / mission_name
412
+ if mission_path is None:
413
+ # Fallback to interpreting the target path directly
414
+ try:
415
+ target = Path(os.readlink(active_mission_link))
416
+ mission_path = (active_mission_link.parent / target).resolve()
417
+ except (OSError, RuntimeError):
418
+ mission_path = None
419
+
420
+ if mission_path is None:
421
+ mission_path = kittify_dir / "missions" / "software-dev"
422
+ else:
423
+ # Default to software-dev if no active mission set
424
+ mission_path = kittify_dir / "missions" / "software-dev"
425
+
426
+ if not mission_path.exists():
427
+ raise MissionNotFoundError(
428
+ f"Active mission directory not found: {mission_path}\n"
429
+ f"Available missions: {list_available_missions(kittify_dir)}"
430
+ )
431
+
432
+ return Mission(mission_path)
433
+
434
+
435
+ def list_available_missions(kittify_dir: Optional[Path] = None) -> List[str]:
436
+ """List all available missions in a project.
437
+
438
+ Args:
439
+ kittify_dir: Path to .kittify directory (defaults to current project)
440
+
441
+ Returns:
442
+ List of mission names (directory names)
443
+ """
444
+ if kittify_dir is None:
445
+ kittify_dir = Path.cwd() / ".kittify"
446
+
447
+ missions_dir = kittify_dir / "missions"
448
+
449
+ if not missions_dir.exists():
450
+ return []
451
+
452
+ missions = []
453
+ for mission_dir in missions_dir.iterdir():
454
+ if mission_dir.is_dir() and (mission_dir / "mission.yaml").exists():
455
+ missions.append(mission_dir.name)
456
+
457
+ return sorted(missions)
458
+
459
+
460
+ def get_mission_by_name(mission_name: str, kittify_dir: Optional[Path] = None) -> Mission:
461
+ """Get a mission by name.
462
+
463
+ Args:
464
+ mission_name: Name of the mission (e.g., 'software-dev', 'research')
465
+ kittify_dir: Path to .kittify directory (defaults to current project)
466
+
467
+ Returns:
468
+ Mission object
469
+
470
+ Raises:
471
+ MissionNotFoundError: If mission doesn't exist
472
+ """
473
+ if kittify_dir is None:
474
+ kittify_dir = Path.cwd() / ".kittify"
475
+
476
+ mission_path = kittify_dir / "missions" / mission_name
477
+
478
+ if not mission_path.exists():
479
+ available = list_available_missions(kittify_dir)
480
+ raise MissionNotFoundError(
481
+ f"Mission '{mission_name}' not found.\n"
482
+ f"Available missions: {', '.join(available) if available else 'none'}"
483
+ )
484
+
485
+ return Mission(mission_path)
486
+
487
+
488
+ def set_active_mission(mission_name: str, kittify_dir: Optional[Path] = None) -> None:
489
+ """DEPRECATED: Set the active mission for a project.
490
+
491
+ .. deprecated:: 0.8.0
492
+ Missions are now selected per-feature during /spec-kitty.specify.
493
+ This function is kept for backwards compatibility but will be removed
494
+ in a future version. Use get_mission_for_feature() instead.
495
+
496
+ Args:
497
+ mission_name: Name of the mission to activate
498
+ kittify_dir: Path to .kittify directory (defaults to current project)
499
+
500
+ Raises:
501
+ MissionNotFoundError: If mission doesn't exist
502
+ """
503
+ import warnings
504
+ warnings.warn(
505
+ "set_active_mission() is deprecated. Missions are now per-feature "
506
+ "and selected during /spec-kitty.specify. This function will be "
507
+ "removed in a future version.",
508
+ DeprecationWarning,
509
+ stacklevel=2
510
+ )
511
+
512
+ if kittify_dir is None:
513
+ kittify_dir = Path.cwd() / ".kittify"
514
+
515
+ # Validate mission exists
516
+ mission = get_mission_by_name(mission_name, kittify_dir)
517
+
518
+ # Create or update symlink
519
+ active_mission_link = kittify_dir / "active-mission"
520
+
521
+ # Remove existing symlink if it exists
522
+ if active_mission_link.exists() or active_mission_link.is_symlink():
523
+ active_mission_link.unlink()
524
+
525
+ # Create new symlink (relative path keeps worktrees portable)
526
+ try:
527
+ active_mission_link.symlink_to(Path("missions") / mission_name)
528
+ except (OSError, NotImplementedError):
529
+ # Fall back to plain file marker when symlinks are unavailable
530
+ active_mission_link.write_text(f"{mission_name}\n", encoding="utf-8")
531
+
532
+
533
+ # =============================================================================
534
+ # Per-Feature Mission Functions (v0.8.0+)
535
+ # =============================================================================
536
+
537
+
538
+ def get_feature_mission_key(feature_dir: Path) -> str:
539
+ """Extract mission key from feature's meta.json, defaulting to software-dev.
540
+
541
+ This is a helper function for reading the mission field from a feature's
542
+ metadata file. It handles missing files and invalid JSON gracefully.
543
+
544
+ Args:
545
+ feature_dir: Path to the feature directory (kitty-specs/<feature>/)
546
+
547
+ Returns:
548
+ Mission key string (e.g., 'software-dev', 'research')
549
+ """
550
+ meta_file = feature_dir / "meta.json"
551
+ if not meta_file.exists():
552
+ return "software-dev"
553
+ try:
554
+ with open(meta_file, 'r', encoding='utf-8') as f:
555
+ meta = json.load(f)
556
+ return meta.get("mission", "software-dev")
557
+ except (json.JSONDecodeError, OSError):
558
+ return "software-dev"
559
+
560
+
561
+ def get_mission_for_feature(feature_dir: Path, project_root: Optional[Path] = None) -> Mission:
562
+ """Get the mission for a specific feature.
563
+
564
+ Reads the mission key from the feature's meta.json and loads the
565
+ corresponding mission. If the mission field is missing or the specified
566
+ mission doesn't exist, falls back to software-dev for backward compatibility.
567
+
568
+ Args:
569
+ feature_dir: Path to the feature directory (kitty-specs/<feature>/)
570
+ project_root: Optional project root (defaults to finding .kittify)
571
+
572
+ Returns:
573
+ Mission object for the feature
574
+
575
+ Raises:
576
+ MissionNotFoundError: If feature meta.json not found and no default available
577
+ """
578
+ # Get the mission key from meta.json
579
+ mission_key = get_feature_mission_key(feature_dir)
580
+
581
+ # Find project root if not provided
582
+ if project_root is None:
583
+ # Walk up from feature_dir to find .kittify
584
+ current = feature_dir.resolve()
585
+ while current != current.parent:
586
+ if (current / ".kittify").exists():
587
+ project_root = current
588
+ break
589
+ current = current.parent
590
+
591
+ if project_root is None:
592
+ raise MissionNotFoundError(
593
+ f"Could not find .kittify directory from {feature_dir}\n"
594
+ f"Is this a Spec Kitty project?"
595
+ )
596
+
597
+ kittify_dir = project_root / ".kittify"
598
+
599
+ # Try to load the specified mission
600
+ try:
601
+ return get_mission_by_name(mission_key, kittify_dir)
602
+ except MissionNotFoundError:
603
+ # Fall back to software-dev with warning
604
+ warnings.warn(
605
+ f"Mission '{mission_key}' not found for feature {feature_dir.name}, "
606
+ f"using software-dev as default",
607
+ stacklevel=2
608
+ )
609
+ return get_mission_by_name("software-dev", kittify_dir)
610
+
611
+
612
+ def discover_missions(project_root: Optional[Path] = None) -> Dict[str, Tuple[Mission, str]]:
613
+ """Discover all available missions with their sources.
614
+
615
+ Scans the project's .kittify/missions/ directory for valid mission
616
+ configurations and returns them with source indicators.
617
+
618
+ Args:
619
+ project_root: Path to project root (defaults to current directory)
620
+
621
+ Returns:
622
+ Dict mapping mission key to (Mission, source) tuple.
623
+ Source is one of: "project", "built-in"
624
+ (Currently both are in the same location, but conceptually distinct)
625
+ """
626
+ if project_root is None:
627
+ project_root = Path.cwd()
628
+
629
+ kittify_dir = project_root / ".kittify"
630
+
631
+ if not kittify_dir.exists():
632
+ return {}
633
+
634
+ missions_dir = kittify_dir / "missions"
635
+
636
+ if not missions_dir.exists():
637
+ return {}
638
+
639
+ missions: Dict[str, Tuple[Mission, str]] = {}
640
+
641
+ for mission_dir in missions_dir.iterdir():
642
+ if mission_dir.is_dir() and (mission_dir / "mission.yaml").exists():
643
+ try:
644
+ mission = Mission(mission_dir)
645
+ # For now, all missions are "project" source
646
+ # (built-in and project share same location in .kittify/missions/)
647
+ missions[mission_dir.name] = (mission, "project")
648
+ except MissionError as e:
649
+ warnings.warn(
650
+ f"Skipping invalid mission '{mission_dir.name}': {e}",
651
+ stacklevel=2
652
+ )
653
+
654
+ return missions