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,848 @@
1
+ #!/usr/bin/env python3
2
+ """CLI utilities for managing Spec Kitty work-package prompts and acceptance."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+ from datetime import datetime, timezone
14
+
15
+ SCRIPT_DIR = Path(__file__).resolve().parent
16
+ if str(SCRIPT_DIR) not in sys.path:
17
+ sys.path.insert(0, str(SCRIPT_DIR))
18
+
19
+ from task_helpers import ( # noqa: E402
20
+ LANES,
21
+ TaskCliError,
22
+ WorkPackage,
23
+ append_activity_log,
24
+ activity_entries,
25
+ build_document,
26
+ detect_conflicting_wp_status,
27
+ ensure_lane,
28
+ find_repo_root,
29
+ get_lane_from_frontmatter,
30
+ git_status_lines,
31
+ is_legacy_format,
32
+ normalize_note,
33
+ now_utc,
34
+ path_has_changes,
35
+ run_git,
36
+ set_scalar,
37
+ split_frontmatter,
38
+ locate_work_package,
39
+ )
40
+ from acceptance_support import ( # noqa: E402
41
+ AcceptanceError,
42
+ AcceptanceResult,
43
+ AcceptanceSummary,
44
+ ArtifactEncodingError,
45
+ choose_mode,
46
+ collect_feature_summary,
47
+ detect_feature_slug,
48
+ normalize_feature_encoding,
49
+ perform_acceptance,
50
+ )
51
+
52
+
53
+ def stage_update(
54
+ repo_root: Path,
55
+ wp: WorkPackage,
56
+ target_lane: str,
57
+ agent: str,
58
+ shell_pid: str,
59
+ note: str,
60
+ timestamp: str,
61
+ dry_run: bool = False,
62
+ ) -> Path:
63
+ """Update work package lane in frontmatter (no file movement).
64
+
65
+ The frontmatter-only lane system keeps all WP files in a flat tasks/ directory.
66
+ Lane changes update the `lane:` field in frontmatter without moving the file.
67
+ """
68
+ if dry_run:
69
+ return wp.path
70
+
71
+ wp.frontmatter = set_scalar(wp.frontmatter, "lane", target_lane)
72
+ wp.frontmatter = set_scalar(wp.frontmatter, "agent", agent)
73
+ if shell_pid:
74
+ wp.frontmatter = set_scalar(wp.frontmatter, "shell_pid", shell_pid)
75
+ log_entry = f"- {timestamp} – {agent} – shell_pid={shell_pid} – lane={target_lane} – {note}"
76
+ new_body = append_activity_log(wp.body, log_entry)
77
+
78
+ new_content = build_document(wp.frontmatter, new_body, wp.padding)
79
+ wp.path.write_text(new_content, encoding="utf-8")
80
+
81
+ run_git(["add", str(wp.path.relative_to(repo_root))], cwd=repo_root, check=True)
82
+
83
+ return wp.path
84
+
85
+
86
+ def _collect_summary_with_encoding(
87
+ repo_root: Path,
88
+ feature: str,
89
+ *,
90
+ strict_metadata: bool,
91
+ normalize_encoding: bool,
92
+ ) -> AcceptanceSummary:
93
+ try:
94
+ return collect_feature_summary(
95
+ repo_root,
96
+ feature,
97
+ strict_metadata=strict_metadata,
98
+ )
99
+ except ArtifactEncodingError as exc:
100
+ if not normalize_encoding:
101
+ raise
102
+ cleaned = normalize_feature_encoding(repo_root, feature)
103
+ if cleaned:
104
+ print("[spec-kitty] Normalized artifact encoding for:", file=sys.stderr)
105
+ for path in cleaned:
106
+ try:
107
+ rel = path.relative_to(repo_root)
108
+ except ValueError:
109
+ rel = path
110
+ print(f" - {rel}", file=sys.stderr)
111
+ else:
112
+ print(
113
+ "[spec-kitty] normalize-encoding enabled but no files required updates.",
114
+ file=sys.stderr,
115
+ )
116
+ return collect_feature_summary(
117
+ repo_root,
118
+ feature,
119
+ strict_metadata=strict_metadata,
120
+ )
121
+
122
+
123
+ def _handle_encoding_failure(exc: ArtifactEncodingError, attempted_fix: bool) -> None:
124
+ print(f"Error: {exc}", file=sys.stderr)
125
+ if attempted_fix:
126
+ print(
127
+ "Encoding issues persist after normalization attempt. Please correct the file manually.",
128
+ file=sys.stderr,
129
+ )
130
+ else:
131
+ print(
132
+ "Re-run with --normalize-encoding to attempt automatic repair.",
133
+ file=sys.stderr,
134
+ )
135
+ sys.exit(1)
136
+
137
+
138
+ _legacy_warning_shown = False
139
+
140
+
141
+ def _check_legacy_format(feature: str, repo_root: Path) -> bool:
142
+ """Check for legacy format and warn once. Returns True if legacy format detected."""
143
+ global _legacy_warning_shown
144
+ feature_path = repo_root / "kitty-specs" / feature
145
+ if is_legacy_format(feature_path):
146
+ if not _legacy_warning_shown:
147
+ print("\n" + "=" * 60, file=sys.stderr)
148
+ print("Legacy directory-based lanes detected.", file=sys.stderr)
149
+ print("", file=sys.stderr)
150
+ print("Your project uses the old lane structure (tasks/planned/, tasks/doing/, etc.).", file=sys.stderr)
151
+ print("Run `spec-kitty upgrade` to migrate to frontmatter-only lanes.", file=sys.stderr)
152
+ print("", file=sys.stderr)
153
+ print("Benefits of upgrading:", file=sys.stderr)
154
+ print(" - No file conflicts during lane changes", file=sys.stderr)
155
+ print(" - Direct editing of lane: field supported", file=sys.stderr)
156
+ print(" - Better multi-agent compatibility", file=sys.stderr)
157
+ print("=" * 60 + "\n", file=sys.stderr)
158
+ _legacy_warning_shown = True
159
+ return True
160
+ return False
161
+
162
+
163
+ def update_command(args: argparse.Namespace) -> None:
164
+ """Update a work package's lane in frontmatter (no file movement)."""
165
+ # Validate lane value first
166
+ try:
167
+ validated_lane = ensure_lane(args.lane)
168
+ except TaskCliError as e:
169
+ print(f"Error: {e}", file=sys.stderr)
170
+ sys.exit(1)
171
+
172
+ repo_root = find_repo_root()
173
+ feature = args.feature
174
+
175
+ # Check for legacy format and error out
176
+ if _check_legacy_format(feature, repo_root):
177
+ print("Error: Cannot use 'update' command on legacy format.", file=sys.stderr)
178
+ print("Run 'spec-kitty upgrade' first, then retry.", file=sys.stderr)
179
+ sys.exit(1)
180
+
181
+ wp = locate_work_package(repo_root, feature, args.work_package)
182
+
183
+ if wp.current_lane == validated_lane:
184
+ raise TaskCliError(f"Work package already in lane '{validated_lane}'.")
185
+
186
+ timestamp = args.timestamp or now_utc()
187
+ agent = args.agent or wp.agent or "system"
188
+ shell_pid = args.shell_pid or wp.shell_pid or ""
189
+ note = normalize_note(args.note, validated_lane)
190
+
191
+ # Stage the update (frontmatter only, no file movement)
192
+ updated_path = stage_update(
193
+ repo_root=repo_root,
194
+ wp=wp,
195
+ target_lane=validated_lane,
196
+ agent=agent,
197
+ shell_pid=shell_pid,
198
+ note=note,
199
+ timestamp=timestamp,
200
+ dry_run=args.dry_run,
201
+ )
202
+
203
+ if args.dry_run:
204
+ print(f"[dry-run] Would update {wp.work_package_id or wp.path.name} to lane '{validated_lane}'")
205
+ print(f"[dry-run] File stays at: {updated_path.relative_to(repo_root)}")
206
+ return
207
+
208
+ print(f"✅ Updated {wp.work_package_id or wp.path.name} → {validated_lane}")
209
+ print(f" {wp.path.relative_to(repo_root)}")
210
+ print(
211
+ f" Logged: - {timestamp} – {agent} – shell_pid={shell_pid} – lane={validated_lane} – {note}"
212
+ )
213
+
214
+
215
+ def history_command(args: argparse.Namespace) -> None:
216
+ repo_root = find_repo_root()
217
+ wp = locate_work_package(repo_root, args.feature, args.work_package)
218
+ agent = args.agent or wp.agent or "system"
219
+ shell_pid = args.shell_pid or wp.shell_pid or ""
220
+ lane = ensure_lane(args.lane or wp.current_lane)
221
+ timestamp = args.timestamp or now_utc()
222
+ note = normalize_note(args.note, lane)
223
+
224
+ if lane != wp.current_lane:
225
+ wp.frontmatter = set_scalar(wp.frontmatter, "lane", lane)
226
+
227
+ log_entry = f"- {timestamp} – {agent} – shell_pid={shell_pid} – lane={lane} – {note}"
228
+ updated_body = append_activity_log(wp.body, log_entry)
229
+
230
+ if args.update_shell and shell_pid:
231
+ wp.frontmatter = set_scalar(wp.frontmatter, "shell_pid", shell_pid)
232
+ if args.assignee is not None:
233
+ wp.frontmatter = set_scalar(wp.frontmatter, "assignee", args.assignee)
234
+ if args.agent:
235
+ wp.frontmatter = set_scalar(wp.frontmatter, "agent", agent)
236
+
237
+ if args.dry_run:
238
+ print(f"[dry-run] Would append activity entry: {log_entry}")
239
+ return
240
+
241
+ new_content = build_document(wp.frontmatter, updated_body, wp.padding)
242
+ wp.path.write_text(new_content, encoding="utf-8")
243
+ run_git(["add", str(wp.path.relative_to(repo_root))], cwd=repo_root, check=True)
244
+
245
+ print(f"📝 Appended activity for {wp.work_package_id or wp.path.name}")
246
+ print(f" {log_entry}")
247
+
248
+
249
+ def list_command(args: argparse.Namespace) -> None:
250
+ repo_root = find_repo_root()
251
+ feature_path = repo_root / "kitty-specs" / args.feature
252
+ feature_dir = feature_path / "tasks"
253
+ if not feature_dir.exists():
254
+ raise TaskCliError(f"Feature '{args.feature}' has no tasks directory at {feature_dir}.")
255
+
256
+ # Check for legacy format and warn
257
+ use_legacy = is_legacy_format(feature_path)
258
+ if use_legacy:
259
+ _check_legacy_format(args.feature, repo_root)
260
+
261
+ rows = []
262
+
263
+ if use_legacy:
264
+ # Legacy format: scan lane subdirectories
265
+ for lane in LANES:
266
+ lane_dir = feature_dir / lane
267
+ if not lane_dir.exists():
268
+ continue
269
+ for path in sorted(lane_dir.rglob("*.md")):
270
+ text = path.read_text(encoding="utf-8-sig")
271
+ front, body, padding = split_frontmatter(text)
272
+ wp = WorkPackage(
273
+ feature=args.feature,
274
+ path=path,
275
+ current_lane=lane,
276
+ relative_subpath=path.relative_to(lane_dir),
277
+ frontmatter=front,
278
+ body=body,
279
+ padding=padding,
280
+ )
281
+ wp_id = wp.work_package_id or path.stem
282
+ title = (wp.title or "").strip('"')
283
+ assignee = (wp.assignee or "").strip()
284
+ agent = (wp.agent or "").strip()
285
+ rows.append(
286
+ {
287
+ "lane": lane,
288
+ "id": wp_id,
289
+ "title": title,
290
+ "assignee": assignee,
291
+ "agent": agent,
292
+ "path": str(path.relative_to(repo_root)),
293
+ }
294
+ )
295
+ else:
296
+ # New format: scan flat tasks/ directory and group by frontmatter lane
297
+ for path in sorted(feature_dir.glob("*.md")):
298
+ if path.name.lower() == "readme.md":
299
+ continue
300
+ text = path.read_text(encoding="utf-8-sig")
301
+ front, body, padding = split_frontmatter(text)
302
+ lane = get_lane_from_frontmatter(path, warn_on_missing=False)
303
+ wp = WorkPackage(
304
+ feature=args.feature,
305
+ path=path,
306
+ current_lane=lane,
307
+ relative_subpath=path.relative_to(feature_dir),
308
+ frontmatter=front,
309
+ body=body,
310
+ padding=padding,
311
+ )
312
+ wp_id = wp.work_package_id or path.stem
313
+ title = (wp.title or "").strip('"')
314
+ assignee = (wp.assignee or "").strip()
315
+ agent = (wp.agent or "").strip()
316
+ rows.append(
317
+ {
318
+ "lane": lane,
319
+ "id": wp_id,
320
+ "title": title,
321
+ "assignee": assignee,
322
+ "agent": agent,
323
+ "path": str(path.relative_to(repo_root)),
324
+ }
325
+ )
326
+
327
+ if not rows:
328
+ print(f"No work packages found for feature '{args.feature}'.")
329
+ return
330
+
331
+ width_id = max(len(row["id"]) for row in rows)
332
+ width_lane = max(len(row["lane"]) for row in rows)
333
+ width_agent = max(len(row["agent"]) for row in rows) if any(row["agent"] for row in rows) else 5
334
+ width_assignee = (
335
+ max(len(row["assignee"]) for row in rows) if any(row["assignee"] for row in rows) else 8
336
+ )
337
+
338
+ header = (
339
+ f"{'Lane'.ljust(width_lane)} "
340
+ f"{'WP'.ljust(width_id)} "
341
+ f"{'Agent'.ljust(width_agent)} "
342
+ f"{'Assignee'.ljust(width_assignee)} "
343
+ "Title"
344
+ )
345
+ print(header)
346
+ print("-" * len(header))
347
+ for row in rows:
348
+ print(
349
+ f"{row['lane'].ljust(width_lane)} "
350
+ f"{row['id'].ljust(width_id)} "
351
+ f"{row['agent'].ljust(width_agent)} "
352
+ f"{row['assignee'].ljust(width_assignee)} "
353
+ f"{row['title']} ({row['path']})"
354
+ )
355
+
356
+
357
+ def rollback_command(args: argparse.Namespace) -> None:
358
+ repo_root = find_repo_root()
359
+ wp = locate_work_package(repo_root, args.feature, args.work_package)
360
+ entries = activity_entries(wp.body)
361
+ if len(entries) < 2:
362
+ raise TaskCliError("Not enough activity entries to determine the previous lane.")
363
+
364
+ previous_lane = ensure_lane(entries[-2]["lane"])
365
+ note = args.note or f"Rolled back to {previous_lane}"
366
+ args_for_update = argparse.Namespace(
367
+ feature=args.feature,
368
+ work_package=args.work_package,
369
+ lane=previous_lane,
370
+ note=note,
371
+ agent=args.agent or entries[-1]["agent"],
372
+ assignee=args.assignee,
373
+ shell_pid=args.shell_pid or entries[-1].get("shell_pid", ""),
374
+ timestamp=args.timestamp or now_utc(),
375
+ dry_run=args.dry_run,
376
+ force=args.force,
377
+ )
378
+ update_command(args_for_update)
379
+
380
+
381
+ def _resolve_feature(repo_root: Path, requested: Optional[str]) -> str:
382
+ if requested:
383
+ return requested
384
+ return detect_feature_slug(repo_root)
385
+
386
+
387
+ def _summary_to_text(summary: AcceptanceSummary) -> List[str]:
388
+ lines: List[str] = []
389
+ lines.append(f"Feature: {summary.feature}")
390
+ lines.append(f"Branch: {summary.branch or 'N/A'}")
391
+ lines.append(f"Worktree: {summary.worktree_root}")
392
+ lines.append("")
393
+ lines.append("Work packages by lane:")
394
+ for lane in LANES:
395
+ items = summary.lanes.get(lane, [])
396
+ lines.append(f" {lane} ({len(items)}): {', '.join(items) if items else '-'}")
397
+ lines.append("")
398
+ outstanding = summary.outstanding()
399
+ if outstanding:
400
+ lines.append("Outstanding items:")
401
+ for key, values in outstanding.items():
402
+ lines.append(f" {key}:")
403
+ for value in values:
404
+ lines.append(f" - {value}")
405
+ else:
406
+ lines.append("All acceptance checks passed.")
407
+ if summary.optional_missing:
408
+ lines.append("")
409
+ lines.append("Optional artifacts missing: " + ", ".join(summary.optional_missing))
410
+ return lines
411
+
412
+
413
+ def status_command(args: argparse.Namespace) -> None:
414
+ repo_root = find_repo_root()
415
+ feature = _resolve_feature(repo_root, args.feature)
416
+ try:
417
+ summary = _collect_summary_with_encoding(
418
+ repo_root,
419
+ feature,
420
+ strict_metadata=not args.lenient,
421
+ normalize_encoding=args.normalize_encoding,
422
+ )
423
+ except ArtifactEncodingError as exc:
424
+ _handle_encoding_failure(exc, args.normalize_encoding)
425
+ return
426
+ if args.json:
427
+ print(json.dumps(summary.to_dict(), indent=2))
428
+ return
429
+ for line in _summary_to_text(summary):
430
+ print(line)
431
+
432
+
433
+ def verify_command(args: argparse.Namespace) -> None:
434
+ repo_root = find_repo_root()
435
+ feature = _resolve_feature(repo_root, args.feature)
436
+ try:
437
+ summary = _collect_summary_with_encoding(
438
+ repo_root,
439
+ feature,
440
+ strict_metadata=not args.lenient,
441
+ normalize_encoding=args.normalize_encoding,
442
+ )
443
+ except ArtifactEncodingError as exc:
444
+ _handle_encoding_failure(exc, args.normalize_encoding)
445
+ return
446
+ if args.json:
447
+ print(json.dumps(summary.to_dict(), indent=2))
448
+ sys.exit(0 if summary.ok else 1)
449
+ lines = _summary_to_text(summary)
450
+ for line in lines:
451
+ print(line)
452
+ sys.exit(0 if summary.ok else 1)
453
+
454
+
455
+ def accept_command(args: argparse.Namespace) -> None:
456
+ repo_root = find_repo_root()
457
+ feature = _resolve_feature(repo_root, args.feature)
458
+ try:
459
+ summary = _collect_summary_with_encoding(
460
+ repo_root,
461
+ feature,
462
+ strict_metadata=not args.lenient,
463
+ normalize_encoding=args.normalize_encoding,
464
+ )
465
+ except ArtifactEncodingError as exc:
466
+ _handle_encoding_failure(exc, args.normalize_encoding)
467
+ return
468
+
469
+ if args.mode == "checklist":
470
+ if args.json:
471
+ print(json.dumps(summary.to_dict(), indent=2))
472
+ else:
473
+ for line in _summary_to_text(summary):
474
+ print(line)
475
+ sys.exit(0 if summary.ok else 1)
476
+
477
+ mode = choose_mode(args.mode, repo_root)
478
+ tests = list(args.test or [])
479
+
480
+ if not summary.ok and not args.allow_fail:
481
+ for line in _summary_to_text(summary):
482
+ print(line)
483
+ print("\n❌ Outstanding items detected. Fix them or re-run with --allow-fail for checklist mode.")
484
+ sys.exit(1)
485
+
486
+ try:
487
+ result = perform_acceptance(
488
+ summary,
489
+ mode=mode,
490
+ actor=args.actor,
491
+ tests=tests,
492
+ auto_commit=not args.no_commit,
493
+ )
494
+ except AcceptanceError as exc:
495
+ print(f"Error: {exc}", file=sys.stderr)
496
+ sys.exit(1)
497
+
498
+ if args.json:
499
+ print(json.dumps(result.to_dict(), indent=2))
500
+ return
501
+
502
+ print(f"✅ Feature '{feature}' accepted at {result.accepted_at} by {result.accepted_by}")
503
+ if result.accept_commit:
504
+ print(f" Acceptance commit: {result.accept_commit}")
505
+ if result.parent_commit:
506
+ print(f" Parent commit: {result.parent_commit}")
507
+ if result.notes:
508
+ print("\nNotes:")
509
+ for note in result.notes:
510
+ print(f" {note}")
511
+ print("\nNext steps:")
512
+ for instruction in result.instructions:
513
+ print(f" - {instruction}")
514
+ if result.cleanup_instructions:
515
+ print("\nCleanup:")
516
+ for instruction in result.cleanup_instructions:
517
+ print(f" - {instruction}")
518
+
519
+
520
+ def _merge_actor(repo_root: Path) -> str:
521
+ configured = run_git(["config", "user.name"], cwd=repo_root, check=False)
522
+ if configured.returncode == 0:
523
+ name = configured.stdout.strip()
524
+ if name:
525
+ return name
526
+ return os.getenv("GIT_AUTHOR_NAME") or os.getenv("USER") or os.getenv("USERNAME") or "system"
527
+
528
+
529
+ def _prepare_merge_metadata(
530
+ repo_root: Path,
531
+ feature: str,
532
+ target: str,
533
+ strategy: str,
534
+ pushed: bool,
535
+ ) -> Optional[Path]:
536
+ feature_dir = repo_root / "kitty-specs" / feature
537
+ feature_dir.mkdir(parents=True, exist_ok=True)
538
+ meta_path = feature_dir / "meta.json"
539
+
540
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
541
+ merged_by = _merge_actor(repo_root)
542
+
543
+ entry: Dict[str, Any] = {
544
+ "merged_at": timestamp,
545
+ "merged_by": merged_by,
546
+ "target": target,
547
+ "strategy": strategy,
548
+ "pushed": pushed,
549
+ "merge_commit": None,
550
+ }
551
+
552
+ meta: Dict[str, Any] = {}
553
+ if meta_path.exists():
554
+ try:
555
+ meta = json.loads(meta_path.read_text(encoding="utf-8-sig"))
556
+ except json.JSONDecodeError:
557
+ meta = {}
558
+
559
+ history = meta.get("merge_history", [])
560
+ if not isinstance(history, list):
561
+ history = []
562
+ history.append(entry)
563
+ if len(history) > 20:
564
+ history = history[-20:]
565
+ meta["merge_history"] = history
566
+
567
+ meta["merged_at"] = timestamp
568
+ meta["merged_by"] = merged_by
569
+ meta["merged_into"] = target
570
+ meta["merged_strategy"] = strategy
571
+ meta["merged_push"] = pushed
572
+
573
+ meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
574
+ return meta_path
575
+
576
+
577
+ def _finalize_merge_metadata(meta_path: Optional[Path], merge_commit: str) -> None:
578
+ if not meta_path or not meta_path.exists():
579
+ return
580
+
581
+ try:
582
+ meta = json.loads(meta_path.read_text(encoding="utf-8-sig"))
583
+ except json.JSONDecodeError:
584
+ meta = {}
585
+
586
+ history = meta.get("merge_history")
587
+ if isinstance(history, list) and history:
588
+ if isinstance(history[-1], dict):
589
+ history[-1]["merge_commit"] = merge_commit
590
+ meta["merged_commit"] = merge_commit
591
+
592
+ meta_path.write_text(json.dumps(meta, indent=2, sort_keys=True) + "\n", encoding="utf-8")
593
+
594
+ def merge_command(args: argparse.Namespace) -> None:
595
+ repo_root = find_repo_root()
596
+ feature = _resolve_feature(repo_root, args.feature)
597
+
598
+ current_branch = run_git([
599
+ "rev-parse",
600
+ "--abbrev-ref",
601
+ "HEAD",
602
+ ], cwd=repo_root, check=True).stdout.strip()
603
+
604
+ if current_branch == args.target:
605
+ raise TaskCliError(
606
+ f"Already on target branch '{args.target}'. Switch to the feature branch before merging."
607
+ )
608
+
609
+ if current_branch != feature:
610
+ raise TaskCliError(
611
+ f"Current branch '{current_branch}' does not match detected feature '{feature}'."
612
+ " Run this command from the feature worktree or specify --feature explicitly."
613
+ )
614
+
615
+ try:
616
+ git_common = run_git(["rev-parse", "--git-common-dir"], cwd=repo_root, check=True).stdout.strip()
617
+ primary_repo_root = Path(git_common).resolve().parent
618
+ except TaskCliError:
619
+ primary_repo_root = Path(repo_root).resolve()
620
+
621
+ repo_root = Path(repo_root).resolve()
622
+ primary_repo_root = primary_repo_root.resolve()
623
+ in_worktree = repo_root != primary_repo_root
624
+
625
+ def ensure_clean(cwd: Path) -> None:
626
+ status = run_git(["status", "--porcelain"], cwd=cwd, check=True).stdout.strip()
627
+ if status:
628
+ raise TaskCliError(
629
+ f"Working directory at {cwd} has uncommitted changes. Commit or stash before merging."
630
+ )
631
+
632
+ ensure_clean(repo_root)
633
+ if in_worktree:
634
+ ensure_clean(primary_repo_root)
635
+
636
+ if args.dry_run:
637
+ steps = ["Planned actions:"]
638
+ steps.append(f" - Checkout {args.target} in {primary_repo_root}")
639
+ steps.append(" - Fetch remote (if configured)")
640
+ if args.strategy == "squash":
641
+ steps.append(f" - Merge {feature} with --squash and commit")
642
+ elif args.strategy == "rebase":
643
+ steps.append(
644
+ f" - Rebase {feature} onto {args.target} manually (command exits before merge)"
645
+ )
646
+ else:
647
+ steps.append(f" - Merge {feature} with --no-ff")
648
+ if args.push:
649
+ steps.append(f" - Push {args.target} to origin (if upstream configured)")
650
+ if in_worktree and args.remove_worktree:
651
+ steps.append(f" - Remove worktree at {repo_root}")
652
+ if args.delete_branch:
653
+ steps.append(f" - Delete branch {feature}")
654
+ print("\n".join(steps))
655
+ return
656
+
657
+ def git(cmd: List[str], *, cwd: Path = primary_repo_root, check: bool = True) -> subprocess.CompletedProcess:
658
+ return run_git(cmd, cwd=cwd, check=check)
659
+
660
+ git(["checkout", args.target])
661
+
662
+ remotes = run_git(["remote"], cwd=primary_repo_root, check=False)
663
+ has_remote = remotes.returncode == 0 and bool(remotes.stdout.strip())
664
+ if has_remote:
665
+ git(["fetch"], check=False)
666
+ pull = git(["pull", "--ff-only"], check=False)
667
+ if pull.returncode != 0:
668
+ raise TaskCliError(
669
+ "Failed to fast-forward target branch. Resolve upstream changes and retry."
670
+ )
671
+
672
+ if args.strategy == "rebase":
673
+ raise TaskCliError(
674
+ "Rebase strategy requires manual steps. Run `git checkout {feature}` followed by `git rebase {args.target}`."
675
+ )
676
+
677
+ meta_path: Optional[Path] = None
678
+ meta_rel: Optional[str] = None
679
+
680
+ if args.strategy == "squash":
681
+ merge_proc = git(["merge", "--squash", feature], check=False)
682
+ if merge_proc.returncode != 0:
683
+ raise TaskCliError(
684
+ "Merge failed. Resolve conflicts manually, commit, then rerun with --keep-worktree --keep-branch."
685
+ )
686
+ meta_path = _prepare_merge_metadata(primary_repo_root, feature, args.target, args.strategy, args.push)
687
+ if meta_path:
688
+ meta_rel = str(meta_path.relative_to(primary_repo_root))
689
+ git(["add", meta_rel])
690
+ git(["commit", "-m", f"Merge feature {feature}"])
691
+ else:
692
+ merge_proc = git(["merge", "--no-ff", "--no-commit", feature], check=False)
693
+ if merge_proc.returncode != 0:
694
+ raise TaskCliError(
695
+ "Merge failed. Resolve conflicts manually, commit, then rerun with --keep-worktree --keep-branch."
696
+ )
697
+ meta_path = _prepare_merge_metadata(primary_repo_root, feature, args.target, args.strategy, args.push)
698
+ if meta_path:
699
+ meta_rel = str(meta_path.relative_to(primary_repo_root))
700
+ git(["add", meta_rel])
701
+ git(["commit", "-m", f"Merge feature {feature}"])
702
+
703
+ if meta_path:
704
+ merge_commit = git(["rev-parse", "HEAD"]).stdout.strip()
705
+ _finalize_merge_metadata(meta_path, merge_commit)
706
+ meta_rel = meta_rel or str(meta_path.relative_to(primary_repo_root))
707
+ git(["add", meta_rel])
708
+ git(["commit", "--amend", "--no-edit"])
709
+
710
+ if args.push and has_remote:
711
+ push_result = git(["push", "origin", args.target], check=False)
712
+ if push_result.returncode != 0:
713
+ raise TaskCliError(f"Merge succeeded but push failed. Run `git push origin {args.target}` manually.")
714
+ elif args.push and not has_remote:
715
+ print("[spec-kitty] Skipping push: no remote configured.", file=sys.stderr)
716
+
717
+ if in_worktree and args.remove_worktree:
718
+ if repo_root.exists():
719
+ git(["worktree", "remove", str(repo_root), "--force"])
720
+
721
+ if args.delete_branch:
722
+ delete = git(["branch", "-d", feature], check=False)
723
+ if delete.returncode != 0:
724
+ git(["branch", "-D", feature])
725
+
726
+ print(f"Merge complete: {feature} -> {args.target}")
727
+ def build_parser() -> argparse.ArgumentParser:
728
+ parser = argparse.ArgumentParser(description="Spec Kitty task utilities")
729
+ subparsers = parser.add_subparsers(dest="command", required=True)
730
+
731
+ update = subparsers.add_parser("update", help="Update a work package's lane in frontmatter")
732
+ update.add_argument("feature", help="Feature directory slug (e.g., 008-awesome-feature)")
733
+ update.add_argument("work_package", help="Work package identifier (e.g., WP03)")
734
+ update.add_argument("lane", help=f"Target lane ({', '.join(LANES)})")
735
+ update.add_argument("--note", help="Activity note to record with the update")
736
+ update.add_argument("--agent", help="Agent identifier to record (defaults to existing agent/system)")
737
+ update.add_argument("--assignee", help="Friendly assignee name to store in frontmatter")
738
+ update.add_argument("--shell-pid", help="Shell PID to capture in frontmatter/history")
739
+ update.add_argument("--timestamp", help="Override UTC timestamp (YYYY-MM-DDTHH:mm:ssZ)")
740
+ update.add_argument("--dry-run", action="store_true", help="Show what would happen without touching files or git")
741
+ update.add_argument("--force", action="store_true", help="Ignore other staged work-package files")
742
+
743
+ history = subparsers.add_parser("history", help="Append a history entry without changing lanes")
744
+ history.add_argument("feature", help="Feature directory slug")
745
+ history.add_argument("work_package", help="Work package identifier (e.g., WP03)")
746
+ history.add_argument("--note", required=True, help="History note to append")
747
+ history.add_argument("--lane", help="Lane to record (defaults to current lane)")
748
+ history.add_argument("--agent", help="Agent identifier (defaults to frontmatter/system)")
749
+ history.add_argument("--assignee", help="Assignee value to set/override")
750
+ history.add_argument("--shell-pid", help="Shell PID to record")
751
+ history.add_argument("--update-shell", action="store_true", help="Persist the provided shell PID to frontmatter")
752
+ history.add_argument("--timestamp", help="Override UTC timestamp")
753
+ history.add_argument("--dry-run", action="store_true", help="Show the log entry without updating files")
754
+
755
+ list_parser = subparsers.add_parser("list", help="List work packages by lane")
756
+ list_parser.add_argument("feature", help="Feature directory slug")
757
+
758
+ rollback = subparsers.add_parser("rollback", help="Return a work package to its prior lane")
759
+ rollback.add_argument("feature", help="Feature directory slug")
760
+ rollback.add_argument("work_package", help="Work package identifier (e.g., WP03)")
761
+ rollback.add_argument("--note", help="History note to record (default: Rolled back to <lane>)")
762
+ rollback.add_argument("--agent", help="Agent identifier to record for the rollback entry")
763
+ rollback.add_argument("--assignee", help="Assignee override to apply")
764
+ rollback.add_argument("--shell-pid", help="Shell PID to capture")
765
+ rollback.add_argument("--timestamp", help="Override UTC timestamp")
766
+ rollback.add_argument("--dry-run", action="store_true", help="Report planned rollback without modifying files")
767
+ rollback.add_argument("--force", action="store_true", help="Ignore other staged work-package files")
768
+
769
+ status = subparsers.add_parser("status", help="Summarize work packages for a feature")
770
+ status.add_argument("--feature", help="Feature directory slug (auto-detect by default)")
771
+ status.add_argument("--json", action="store_true", help="Emit JSON summary")
772
+ status.add_argument("--lenient", action="store_true", help="Skip strict metadata validation")
773
+ status.add_argument(
774
+ "--normalize-encoding",
775
+ action="store_true",
776
+ help="Automatically repair non-UTF-8 artifact files",
777
+ )
778
+
779
+ verify = subparsers.add_parser("verify", help="Run acceptance checks without committing")
780
+ verify.add_argument("--feature", help="Feature directory slug (auto-detect by default)")
781
+ verify.add_argument("--json", action="store_true", help="Emit JSON summary")
782
+ verify.add_argument("--lenient", action="store_true", help="Skip strict metadata validation")
783
+ verify.add_argument(
784
+ "--normalize-encoding",
785
+ action="store_true",
786
+ help="Automatically repair non-UTF-8 artifact files",
787
+ )
788
+
789
+ accept = subparsers.add_parser("accept", help="Perform feature acceptance workflow")
790
+ accept.add_argument("--feature", help="Feature directory slug (auto-detect by default)")
791
+ accept.add_argument("--mode", choices=["auto", "pr", "local", "checklist"], default="auto")
792
+ accept.add_argument("--actor", help="Override acceptance author (defaults to system/user)")
793
+ accept.add_argument("--test", action="append", help="Record validation command executed (repeatable)")
794
+ accept.add_argument("--json", action="store_true", help="Emit JSON result")
795
+ accept.add_argument("--lenient", action="store_true", help="Skip strict metadata validation")
796
+ accept.add_argument("--no-commit", action="store_true", help="Skip auto-commit (report only)")
797
+ accept.add_argument("--allow-fail", action="store_true", help="Allow outstanding issues (for manual workflows)")
798
+ accept.add_argument(
799
+ "--normalize-encoding",
800
+ action="store_true",
801
+ help="Automatically repair non-UTF-8 artifact files before acceptance",
802
+ )
803
+
804
+ merge = subparsers.add_parser("merge", help="Merge a feature branch into the target branch")
805
+ merge.add_argument("--feature", help="Feature directory slug (auto-detect by default)")
806
+ merge.add_argument("--strategy", choices=["merge", "squash", "rebase"], default="merge")
807
+ merge.add_argument("--target", default="main", help="Target branch to merge into")
808
+ merge.add_argument("--push", action="store_true", help="Push to origin after merging")
809
+ merge.add_argument("--delete-branch", dest="delete_branch", action="store_true", default=True)
810
+ merge.add_argument("--keep-branch", dest="delete_branch", action="store_false")
811
+ merge.add_argument("--remove-worktree", dest="remove_worktree", action="store_true", default=True)
812
+ merge.add_argument("--keep-worktree", dest="remove_worktree", action="store_false")
813
+ merge.add_argument("--dry-run", action="store_true", help="Show actions without executing")
814
+
815
+ return parser
816
+
817
+
818
+ def main(argv: Optional[List[str]] = None) -> int:
819
+ parser = build_parser()
820
+ args = parser.parse_args(argv)
821
+ try:
822
+ if args.command == "update":
823
+ update_command(args)
824
+ elif args.command == "history":
825
+ history_command(args)
826
+ elif args.command == "list":
827
+ list_command(args)
828
+ elif args.command == "rollback":
829
+ rollback_command(args)
830
+ elif args.command == "status":
831
+ status_command(args)
832
+ elif args.command == "verify":
833
+ verify_command(args)
834
+ elif args.command == "merge":
835
+ merge_command(args)
836
+ elif args.command == "accept":
837
+ accept_command(args)
838
+ else:
839
+ parser.error(f"Unknown command {args.command}")
840
+ return 1
841
+ except TaskCliError as exc:
842
+ print(f"Error: {exc}", file=sys.stderr)
843
+ return 1
844
+ return 0
845
+
846
+
847
+ if __name__ == "__main__":
848
+ sys.exit(main())