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
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env python3
2
+ """Acceptance workflow utilities for Spec Kitty features."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple
13
+
14
+ from .tasks_support import (
15
+ LANES,
16
+ TaskCliError,
17
+ WorkPackage,
18
+ activity_entries,
19
+ extract_scalar,
20
+ find_repo_root,
21
+ get_lane_from_frontmatter,
22
+ git_status_lines,
23
+ is_legacy_format,
24
+ run_git,
25
+ split_frontmatter,
26
+ )
27
+ from specify_cli.mission import MissionError, get_mission_for_feature
28
+ from specify_cli.validators.paths import PathValidationError, validate_mission_paths
29
+
30
+ AcceptanceMode = str # Expected values: "pr", "local", "checklist"
31
+
32
+
33
+ class AcceptanceError(TaskCliError):
34
+ """Raised when acceptance cannot complete due to outstanding issues."""
35
+
36
+
37
+ @dataclass
38
+ class WorkPackageState:
39
+ work_package_id: str
40
+ lane: str
41
+ title: str
42
+ path: str
43
+ has_lane_entry: bool
44
+ latest_lane: Optional[str]
45
+ metadata: Dict[str, Optional[str]] = field(default_factory=dict)
46
+
47
+
48
+ @dataclass
49
+ class AcceptanceSummary:
50
+ feature: str
51
+ repo_root: Path
52
+ feature_dir: Path
53
+ tasks_dir: Path
54
+ branch: Optional[str]
55
+ worktree_root: Path
56
+ primary_repo_root: Path
57
+ lanes: Dict[str, List[str]]
58
+ work_packages: List[WorkPackageState]
59
+ metadata_issues: List[str]
60
+ activity_issues: List[str]
61
+ unchecked_tasks: List[str]
62
+ needs_clarification: List[str]
63
+ missing_artifacts: List[str]
64
+ optional_missing: List[str]
65
+ git_dirty: List[str]
66
+ path_violations: List[str]
67
+ warnings: List[str]
68
+
69
+ @property
70
+ def all_done(self) -> bool:
71
+ return not (
72
+ self.lanes.get("planned")
73
+ or self.lanes.get("doing")
74
+ or self.lanes.get("for_review")
75
+ )
76
+
77
+ @property
78
+ def ok(self) -> bool:
79
+ return (
80
+ self.all_done
81
+ and not self.metadata_issues
82
+ and not self.activity_issues
83
+ and not self.unchecked_tasks
84
+ and not self.needs_clarification
85
+ and not self.missing_artifacts
86
+ and not self.git_dirty
87
+ and not self.path_violations
88
+ )
89
+
90
+ def outstanding(self) -> Dict[str, List[str]]:
91
+ buckets = {
92
+ "not_done": [
93
+ *self.lanes.get("planned", []),
94
+ *self.lanes.get("doing", []),
95
+ *self.lanes.get("for_review", []),
96
+ ],
97
+ "metadata": self.metadata_issues,
98
+ "activity": self.activity_issues,
99
+ "unchecked_tasks": self.unchecked_tasks,
100
+ "needs_clarification": self.needs_clarification,
101
+ "missing_artifacts": self.missing_artifacts,
102
+ "git_dirty": self.git_dirty,
103
+ "path_violations": self.path_violations,
104
+ }
105
+ return {key: value for key, value in buckets.items() if value}
106
+
107
+ def to_dict(self) -> Dict[str, object]:
108
+ return {
109
+ "feature": self.feature,
110
+ "branch": self.branch,
111
+ "repo_root": str(self.repo_root),
112
+ "feature_dir": str(self.feature_dir),
113
+ "tasks_dir": str(self.tasks_dir),
114
+ "worktree_root": str(self.worktree_root),
115
+ "primary_repo_root": str(self.primary_repo_root),
116
+ "lanes": self.lanes,
117
+ "work_packages": [
118
+ {
119
+ "id": wp.work_package_id,
120
+ "lane": wp.lane,
121
+ "title": wp.title,
122
+ "path": wp.path,
123
+ "latest_lane": wp.latest_lane,
124
+ "has_lane_entry": wp.has_lane_entry,
125
+ "metadata": wp.metadata,
126
+ }
127
+ for wp in self.work_packages
128
+ ],
129
+ "metadata_issues": self.metadata_issues,
130
+ "activity_issues": self.activity_issues,
131
+ "unchecked_tasks": self.unchecked_tasks,
132
+ "needs_clarification": self.needs_clarification,
133
+ "missing_artifacts": self.missing_artifacts,
134
+ "optional_missing": self.optional_missing,
135
+ "git_dirty": self.git_dirty,
136
+ "path_violations": self.path_violations,
137
+ "warnings": self.warnings,
138
+ "all_done": self.all_done,
139
+ "ok": self.ok,
140
+ }
141
+
142
+
143
+ @dataclass
144
+ class AcceptanceResult:
145
+ summary: AcceptanceSummary
146
+ mode: AcceptanceMode
147
+ accepted_at: str
148
+ accepted_by: str
149
+ parent_commit: Optional[str]
150
+ accept_commit: Optional[str]
151
+ commit_created: bool
152
+ instructions: List[str]
153
+ cleanup_instructions: List[str]
154
+ notes: List[str] = field(default_factory=list)
155
+
156
+ def to_dict(self) -> Dict[str, object]:
157
+ return {
158
+ "accepted_at": self.accepted_at,
159
+ "accepted_by": self.accepted_by,
160
+ "mode": self.mode,
161
+ "parent_commit": self.parent_commit,
162
+ "accept_commit": self.accept_commit,
163
+ "commit_created": self.commit_created,
164
+ "instructions": self.instructions,
165
+ "cleanup_instructions": self.cleanup_instructions,
166
+ "notes": self.notes,
167
+ "summary": self.summary.to_dict(),
168
+ }
169
+
170
+
171
+ def _iter_work_packages(repo_root: Path, feature: str) -> Iterable[WorkPackage]:
172
+ """Iterate over work packages, supporting both legacy and new formats.
173
+
174
+ Legacy format: WP files in tasks/{lane}/ subdirectories
175
+ New format: WP files in flat tasks/ directory with lane in frontmatter
176
+ """
177
+ feature_path = repo_root / "kitty-specs" / feature
178
+ tasks_dir = feature_path / "tasks"
179
+ if not tasks_dir.exists():
180
+ raise AcceptanceError(f"Feature '{feature}' has no tasks directory at {tasks_dir}.")
181
+
182
+ use_legacy = is_legacy_format(feature_path)
183
+
184
+ if use_legacy:
185
+ # Legacy format: iterate over lane subdirectories
186
+ for lane_dir in sorted(tasks_dir.iterdir()):
187
+ if not lane_dir.is_dir():
188
+ continue
189
+ lane = lane_dir.name
190
+ if lane not in LANES:
191
+ continue
192
+ for path in sorted(lane_dir.rglob("*.md")):
193
+ text = path.read_text(encoding="utf-8-sig")
194
+ front, body, padding = split_frontmatter(text)
195
+ relative = path.relative_to(lane_dir)
196
+ yield WorkPackage(
197
+ feature=feature,
198
+ path=path,
199
+ current_lane=lane,
200
+ relative_subpath=relative,
201
+ frontmatter=front,
202
+ body=body,
203
+ padding=padding,
204
+ )
205
+ else:
206
+ # New format: flat tasks/ directory, lane from frontmatter
207
+ for path in sorted(tasks_dir.glob("*.md")):
208
+ if path.name.lower() == "readme.md":
209
+ continue
210
+ text = path.read_text(encoding="utf-8-sig")
211
+ front, body, padding = split_frontmatter(text)
212
+ # Get lane from frontmatter
213
+ lane = get_lane_from_frontmatter(path, warn_on_missing=False)
214
+ relative = path.relative_to(tasks_dir)
215
+ yield WorkPackage(
216
+ feature=feature,
217
+ path=path,
218
+ current_lane=lane,
219
+ relative_subpath=relative,
220
+ frontmatter=front,
221
+ body=body,
222
+ padding=padding,
223
+ )
224
+
225
+
226
+ def detect_feature_slug(
227
+ repo_root: Path,
228
+ *,
229
+ env: Optional[Mapping[str, str]] = None,
230
+ cwd: Optional[Path] = None,
231
+ ) -> str:
232
+ env = env or os.environ
233
+ if "SPECIFY_FEATURE" in env and env["SPECIFY_FEATURE"].strip():
234
+ return env["SPECIFY_FEATURE"].strip()
235
+
236
+ try:
237
+ branch = (
238
+ run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
239
+ .stdout.strip()
240
+ )
241
+ if branch and branch != "HEAD" and re.match(r"^\d{3}-", branch):
242
+ return branch
243
+ except TaskCliError:
244
+ pass
245
+
246
+ cwd = (cwd or Path.cwd()).resolve()
247
+ for parent in [cwd, *cwd.parents]:
248
+ if parent.name.startswith(".worktrees"):
249
+ parts = list(parent.parts)
250
+ try:
251
+ idx = parts.index(".worktrees")
252
+ candidate = parts[idx + 1]
253
+ if re.match(r"^\d{3}-", candidate):
254
+ return candidate
255
+ except (ValueError, IndexError):
256
+ continue
257
+ if parent.name.startswith("0") and re.match(r"^\d{3}-", parent.name):
258
+ return parent.name
259
+
260
+ raise AcceptanceError(
261
+ "Unable to determine feature slug automatically. Provide --feature explicitly."
262
+ )
263
+
264
+
265
+ def _read_file(path: Path) -> str:
266
+ return path.read_text(encoding="utf-8-sig") if path.exists() else ""
267
+
268
+
269
+ def _find_unchecked_tasks(tasks_file: Path) -> List[str]:
270
+ if not tasks_file.exists():
271
+ return ["tasks.md missing"]
272
+
273
+ unchecked: List[str] = []
274
+ for line in tasks_file.read_text(encoding="utf-8-sig").splitlines():
275
+ if re.match(r"^\s*-\s*\[ \]", line):
276
+ unchecked.append(line.strip())
277
+ return unchecked
278
+
279
+
280
+ def _check_needs_clarification(files: Sequence[Path]) -> List[str]:
281
+ results: List[str] = []
282
+ for file_path in files:
283
+ if file_path.exists():
284
+ text = file_path.read_text(encoding="utf-8-sig")
285
+ if "[NEEDS CLARIFICATION" in text:
286
+ results.append(str(file_path))
287
+ return results
288
+
289
+
290
+ def _missing_artifacts(feature_dir: Path) -> Tuple[List[str], List[str]]:
291
+ required = [feature_dir / "spec.md", feature_dir / "plan.md", feature_dir / "tasks.md"]
292
+ optional = [
293
+ feature_dir / "quickstart.md",
294
+ feature_dir / "data-model.md",
295
+ feature_dir / "research.md",
296
+ feature_dir / "contracts",
297
+ ]
298
+ missing_required = [str(p.relative_to(feature_dir)) for p in required if not p.exists()]
299
+ missing_optional = [str(p.relative_to(feature_dir)) for p in optional if not p.exists()]
300
+ return missing_required, missing_optional
301
+
302
+
303
+ def collect_feature_summary(
304
+ repo_root: Path,
305
+ feature: str,
306
+ *,
307
+ strict_metadata: bool = True,
308
+ ) -> AcceptanceSummary:
309
+ feature_dir = repo_root / "kitty-specs" / feature
310
+ tasks_dir = feature_dir / "tasks"
311
+ if not feature_dir.exists():
312
+ raise AcceptanceError(f"Feature directory not found: {feature_dir}")
313
+
314
+ branch: Optional[str] = None
315
+ try:
316
+ branch_value = (
317
+ run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root, check=True)
318
+ .stdout.strip()
319
+ )
320
+ if branch_value and branch_value != "HEAD":
321
+ branch = branch_value
322
+ except TaskCliError:
323
+ branch = None
324
+
325
+ try:
326
+ worktree_root = Path(
327
+ run_git(["rev-parse", "--show-toplevel"], cwd=repo_root, check=True)
328
+ .stdout.strip()
329
+ ).resolve()
330
+ except TaskCliError:
331
+ worktree_root = repo_root
332
+
333
+ try:
334
+ git_common_dir = Path(
335
+ run_git(["rev-parse", "--git-common-dir"], cwd=repo_root, check=True)
336
+ .stdout.strip()
337
+ ).resolve()
338
+ primary_repo_root = git_common_dir.parent
339
+ except TaskCliError:
340
+ primary_repo_root = repo_root
341
+
342
+ lanes: Dict[str, List[str]] = {lane: [] for lane in LANES}
343
+ work_packages: List[WorkPackageState] = []
344
+ metadata_issues: List[str] = []
345
+ activity_issues: List[str] = []
346
+
347
+ use_legacy = is_legacy_format(feature_dir)
348
+
349
+ for wp in _iter_work_packages(repo_root, feature):
350
+ wp_id = wp.work_package_id or wp.path.stem
351
+ title = (wp.title or "").strip('"')
352
+ lanes[wp.current_lane].append(wp_id)
353
+
354
+ entries = activity_entries(wp.body)
355
+ lanes_logged = {entry["lane"] for entry in entries}
356
+ latest_lane = entries[-1]["lane"] if entries else None
357
+ has_lane_entry = wp.current_lane in lanes_logged
358
+
359
+ metadata: Dict[str, Optional[str]] = {
360
+ "lane": wp.lane,
361
+ "agent": wp.agent,
362
+ "assignee": wp.assignee,
363
+ "shell_pid": wp.shell_pid,
364
+ }
365
+
366
+ if strict_metadata:
367
+ lane_value = (wp.lane or "").strip()
368
+ if not lane_value:
369
+ metadata_issues.append(f"{wp_id}: missing lane in frontmatter")
370
+ elif use_legacy and lane_value != wp.current_lane:
371
+ # Only check directory/frontmatter mismatch in legacy format
372
+ metadata_issues.append(
373
+ f"{wp_id}: frontmatter lane '{lane_value}' does not match directory '{wp.current_lane}'"
374
+ )
375
+
376
+ if not wp.agent:
377
+ metadata_issues.append(f"{wp_id}: missing agent in frontmatter")
378
+ if wp.current_lane in {"doing", "for_review", "done"} and not wp.assignee:
379
+ metadata_issues.append(f"{wp_id}: missing assignee in frontmatter")
380
+ if not wp.shell_pid:
381
+ metadata_issues.append(f"{wp_id}: missing shell_pid in frontmatter")
382
+
383
+ if not entries:
384
+ activity_issues.append(f"{wp_id}: Activity Log missing entries")
385
+ else:
386
+ if wp.current_lane not in lanes_logged:
387
+ activity_issues.append(
388
+ f"{wp_id}: Activity Log missing entry for lane={wp.current_lane}"
389
+ )
390
+ if wp.current_lane == "done" and entries[-1]["lane"] != "done":
391
+ activity_issues.append(f"{wp_id}: latest Activity Log entry not lane=done")
392
+
393
+ work_packages.append(
394
+ WorkPackageState(
395
+ work_package_id=wp_id,
396
+ lane=wp.current_lane,
397
+ title=title,
398
+ path=str(wp.path.relative_to(repo_root)),
399
+ has_lane_entry=has_lane_entry,
400
+ latest_lane=latest_lane,
401
+ metadata=metadata,
402
+ )
403
+ )
404
+
405
+ unchecked_tasks = _find_unchecked_tasks(feature_dir / "tasks.md")
406
+ needs_clarification = _check_needs_clarification(
407
+ [
408
+ feature_dir / "spec.md",
409
+ feature_dir / "plan.md",
410
+ feature_dir / "quickstart.md",
411
+ feature_dir / "tasks.md",
412
+ feature_dir / "research.md",
413
+ feature_dir / "data-model.md",
414
+ ]
415
+ )
416
+ missing_required, missing_optional = _missing_artifacts(feature_dir)
417
+
418
+ try:
419
+ git_dirty = git_status_lines(repo_root)
420
+ except TaskCliError:
421
+ git_dirty = []
422
+
423
+ path_violations: List[str] = []
424
+ try:
425
+ mission = get_mission_for_feature(feature_dir)
426
+ except MissionError:
427
+ mission = None
428
+
429
+ if mission and mission.config.paths:
430
+ try:
431
+ validate_mission_paths(mission, repo_root, strict=True)
432
+ except PathValidationError as exc:
433
+ message = exc.result.format_errors() or str(exc)
434
+ path_violations.append(message)
435
+
436
+ warnings: List[str] = []
437
+ if missing_optional:
438
+ warnings.append(
439
+ "Optional artifacts missing: " + ", ".join(missing_optional)
440
+ )
441
+ if path_violations:
442
+ warnings.append("Path conventions not satisfied.")
443
+
444
+ return AcceptanceSummary(
445
+ feature=feature,
446
+ repo_root=repo_root,
447
+ feature_dir=feature_dir,
448
+ tasks_dir=tasks_dir,
449
+ branch=branch,
450
+ worktree_root=worktree_root,
451
+ primary_repo_root=primary_repo_root,
452
+ lanes=lanes,
453
+ work_packages=work_packages,
454
+ metadata_issues=metadata_issues,
455
+ activity_issues=activity_issues,
456
+ unchecked_tasks=unchecked_tasks if unchecked_tasks != ["tasks.md missing"] else [],
457
+ needs_clarification=needs_clarification,
458
+ missing_artifacts=missing_required,
459
+ optional_missing=missing_optional,
460
+ git_dirty=git_dirty,
461
+ path_violations=path_violations,
462
+ warnings=warnings,
463
+ )
464
+
465
+
466
+ def choose_mode(preference: Optional[str], repo_root: Path) -> AcceptanceMode:
467
+ if preference in {"pr", "local", "checklist"}:
468
+ return preference
469
+ try:
470
+ remotes = (
471
+ run_git(["remote"], cwd=repo_root, check=False).stdout.strip().splitlines()
472
+ )
473
+ if remotes:
474
+ return "pr"
475
+ except TaskCliError:
476
+ pass
477
+ return "local"
478
+
479
+
480
+ def perform_acceptance(
481
+ summary: AcceptanceSummary,
482
+ *,
483
+ mode: AcceptanceMode,
484
+ actor: Optional[str],
485
+ tests: Optional[Sequence[str]] = None,
486
+ auto_commit: bool = True,
487
+ ) -> AcceptanceResult:
488
+ if mode != "checklist" and not summary.ok:
489
+ raise AcceptanceError(
490
+ "Acceptance checks failed; run verify to see outstanding issues."
491
+ )
492
+
493
+ actor_name = (actor or os.getenv("USER") or os.getenv("USERNAME") or "system").strip()
494
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
495
+
496
+ parent_commit: Optional[str] = None
497
+ accept_commit: Optional[str] = None
498
+
499
+ if auto_commit and mode != "checklist":
500
+ try:
501
+ parent_commit = (
502
+ run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=False)
503
+ .stdout.strip()
504
+ or None
505
+ )
506
+ except TaskCliError:
507
+ parent_commit = None
508
+
509
+ meta_path = summary.feature_dir / "meta.json"
510
+ if meta_path.exists():
511
+ meta = json.loads(meta_path.read_text(encoding="utf-8-sig"))
512
+ else:
513
+ meta = {}
514
+
515
+ acceptance_record: Dict[str, object] = {
516
+ "accepted_at": timestamp,
517
+ "accepted_by": actor_name,
518
+ "mode": mode,
519
+ "branch": summary.branch,
520
+ "accepted_from_commit": parent_commit,
521
+ }
522
+ if tests:
523
+ acceptance_record["validation_commands"] = list(tests)
524
+
525
+ meta["accepted_at"] = timestamp
526
+ meta["accepted_by"] = actor_name
527
+ meta["acceptance_mode"] = mode
528
+ meta["accepted_from_commit"] = parent_commit
529
+ meta["accept_commit"] = None
530
+
531
+ history: List[Dict[str, object]] = meta.setdefault("acceptance_history", [])
532
+ history.append(acceptance_record)
533
+ # limit history to last 20 entries
534
+ if len(history) > 20:
535
+ meta["acceptance_history"] = history[-20:]
536
+
537
+ meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
538
+ run_git(
539
+ ["add", str(meta_path.relative_to(summary.repo_root))],
540
+ cwd=summary.repo_root,
541
+ check=True,
542
+ )
543
+
544
+ status = run_git(["diff", "--cached", "--name-only"], cwd=summary.repo_root, check=True)
545
+ staged_files = [line.strip() for line in status.stdout.splitlines() if line.strip()]
546
+ commit_created = False
547
+ if staged_files:
548
+ commit_msg = f"Accept {summary.feature}"
549
+ run_git(["commit", "-m", commit_msg], cwd=summary.repo_root, check=True)
550
+ commit_created = True
551
+ try:
552
+ accept_commit = (
553
+ run_git(["rev-parse", "HEAD"], cwd=summary.repo_root, check=True)
554
+ .stdout.strip()
555
+ )
556
+ except TaskCliError:
557
+ accept_commit = None
558
+ else:
559
+ commit_created = False
560
+ else:
561
+ commit_created = False
562
+
563
+ instructions: List[str] = []
564
+ cleanup_instructions: List[str] = []
565
+
566
+ branch = summary.branch or summary.feature
567
+ if mode == "pr":
568
+ instructions.extend(
569
+ [
570
+ f"Review the acceptance commit on branch `{branch}`.",
571
+ f"Push your branch: `git push origin {branch}`",
572
+ "Open a pull request referencing spec/plan/tasks artifacts.",
573
+ "Include acceptance summary and test evidence in the PR description.",
574
+ ]
575
+ )
576
+ elif mode == "local":
577
+ instructions.extend(
578
+ [
579
+ "Switch to your integration branch (e.g., `git checkout main`).",
580
+ "Synchronize it (e.g., `git pull --ff-only`).",
581
+ f"Merge the feature: `git merge {branch}`",
582
+ ]
583
+ )
584
+ else: # checklist
585
+ instructions.append(
586
+ "All checks passed. Proceed with your manual acceptance workflow."
587
+ )
588
+
589
+ if summary.worktree_root != summary.primary_repo_root:
590
+ cleanup_instructions.append(
591
+ f"After merging, remove the worktree: `git worktree remove {summary.worktree_root}`"
592
+ )
593
+ cleanup_instructions.append(f"Delete the feature branch when done: `git branch -d {branch}`")
594
+
595
+ notes: List[str] = []
596
+ if accept_commit:
597
+ notes.append(f"Acceptance commit: {accept_commit}")
598
+ if parent_commit:
599
+ notes.append(f"Accepted from parent commit: {parent_commit}")
600
+ if tests:
601
+ notes.append("Validation commands:")
602
+ notes.extend(f" - {cmd}" for cmd in tests)
603
+
604
+ return AcceptanceResult(
605
+ summary=summary,
606
+ mode=mode,
607
+ accepted_at=timestamp,
608
+ accepted_by=actor_name,
609
+ parent_commit=parent_commit,
610
+ accept_commit=accept_commit,
611
+ commit_created=commit_created,
612
+ instructions=instructions,
613
+ cleanup_instructions=cleanup_instructions,
614
+ notes=notes,
615
+ )
616
+
617
+
618
+ __all__ = [
619
+ "AcceptanceError",
620
+ "AcceptanceResult",
621
+ "AcceptanceSummary",
622
+ "AcceptanceMode",
623
+ "collect_feature_summary",
624
+ "detect_feature_slug",
625
+ "choose_mode",
626
+ "perform_acceptance",
627
+ ]