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,180 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate and fix UTF-8 encoding in Spec Kitty markdown files.
4
+
5
+ This utility helps prevent UnicodeDecodeError issues by:
6
+ 1. Scanning markdown files for encoding problems
7
+ 2. Detecting likely encoding (UTF-8, Windows-1252, etc.)
8
+ 3. Converting files to UTF-8 if needed
9
+ 4. Reporting encoding issues with specific positions
10
+
11
+ Usage:
12
+ python scripts/validate_encoding.py --check kitty-specs/
13
+ python scripts/validate_encoding.py --fix kitty-specs/001-feature/
14
+ python scripts/validate_encoding.py --scan-all
15
+ """
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import List, Tuple, Optional
21
+
22
+
23
+ def check_utf8_encoding(file_path: Path) -> Tuple[bool, Optional[str]]:
24
+ """
25
+ Check if a file is valid UTF-8.
26
+
27
+ Returns:
28
+ (is_valid, error_message)
29
+ """
30
+ try:
31
+ with open(file_path, 'r', encoding='utf-8') as f:
32
+ f.read()
33
+ return (True, None)
34
+ except UnicodeDecodeError as e:
35
+ error_msg = f"Position {e.start}: {e.reason} (byte 0x{e.object[e.start]:02x})"
36
+ return (False, error_msg)
37
+ except Exception as e:
38
+ return (False, str(e))
39
+
40
+
41
+ def detect_encoding(file_path: Path) -> str:
42
+ """
43
+ Try to detect the file's actual encoding.
44
+
45
+ Returns encoding name or 'unknown'.
46
+ """
47
+ encodings = ['utf-8', 'windows-1252', 'iso-8859-1', 'utf-16', 'utf-32']
48
+
49
+ for encoding in encodings:
50
+ try:
51
+ with open(file_path, 'r', encoding=encoding) as f:
52
+ f.read()
53
+ return encoding
54
+ except (UnicodeDecodeError, UnicodeError):
55
+ continue
56
+
57
+ return 'unknown'
58
+
59
+
60
+ def convert_to_utf8(file_path: Path, source_encoding: str = 'windows-1252', dry_run: bool = False) -> bool:
61
+ """
62
+ Convert a file from source_encoding to UTF-8.
63
+
64
+ Returns True if successful.
65
+ """
66
+ try:
67
+ with open(file_path, 'rb') as f:
68
+ data = f.read()
69
+
70
+ # Decode with source encoding
71
+ text = data.decode(source_encoding, errors='replace')
72
+
73
+ # Common Windows-1252 → UTF-8 fixes
74
+ text = text.replace('\u0086\u0092', '→') # Dagger + right-quote = arrow
75
+ text = text.replace('\u0093', '→') # Sometimes used as arrow
76
+ text = text.replace('\u0094', '"') # Right double quote
77
+ text = text.replace('\u0091', "'") # Left single quote
78
+ text = text.replace('\u0092', "'") # Right single quote
79
+
80
+ if dry_run:
81
+ print(f" [DRY RUN] Would convert {file_path.name}")
82
+ return True
83
+
84
+ # Write as UTF-8
85
+ with open(file_path, 'w', encoding='utf-8') as f:
86
+ f.write(text)
87
+
88
+ return True
89
+ except Exception as e:
90
+ print(f" ❌ Conversion failed: {e}")
91
+ return False
92
+
93
+
94
+ def scan_directory(directory: Path, fix: bool = False, dry_run: bool = False) -> List[Path]:
95
+ """
96
+ Scan directory for markdown files with encoding issues.
97
+
98
+ Returns list of files with problems.
99
+ """
100
+ problem_files = []
101
+
102
+ markdown_files = list(directory.rglob('*.md'))
103
+
104
+ if not markdown_files:
105
+ print(f"No markdown files found in {directory}")
106
+ return []
107
+
108
+ print(f"\nScanning {len(markdown_files)} markdown files in {directory}...\n")
109
+
110
+ for md_file in markdown_files:
111
+ is_valid, error = check_utf8_encoding(md_file)
112
+
113
+ if is_valid:
114
+ print(f"✅ {md_file.relative_to(directory)}")
115
+ else:
116
+ print(f"❌ {md_file.relative_to(directory)}")
117
+ print(f" Error: {error}")
118
+
119
+ if fix or dry_run:
120
+ detected = detect_encoding(md_file)
121
+ print(f" Detected encoding: {detected}")
122
+
123
+ if detected != 'utf-8' and detected != 'unknown':
124
+ if convert_to_utf8(md_file, detected, dry_run):
125
+ if not dry_run:
126
+ # Verify the fix worked
127
+ is_valid_now, _ = check_utf8_encoding(md_file)
128
+ if is_valid_now:
129
+ print(f" ✅ Fixed! Converted from {detected} to UTF-8")
130
+ else:
131
+ print(f" ⚠️ Conversion completed but file still has issues")
132
+ problem_files.append(md_file)
133
+ else:
134
+ problem_files.append(md_file)
135
+ else:
136
+ problem_files.append(md_file)
137
+
138
+ print()
139
+
140
+ return problem_files
141
+
142
+
143
+ def main():
144
+ parser = argparse.ArgumentParser(description='Validate UTF-8 encoding in markdown files')
145
+ parser.add_argument('path', nargs='?', default='kitty-specs', help='Path to scan (default: kitty-specs)')
146
+ parser.add_argument('--fix', action='store_true', help='Attempt to fix encoding issues')
147
+ parser.add_argument('--dry-run', action='store_true', help='Show what would be fixed without making changes')
148
+ parser.add_argument('--scan-all', action='store_true', help='Scan entire repository')
149
+
150
+ args = parser.parse_args()
151
+
152
+ if args.scan_all:
153
+ scan_path = Path.cwd()
154
+ else:
155
+ scan_path = Path(args.path)
156
+
157
+ if not scan_path.exists():
158
+ print(f"❌ Error: Path does not exist: {scan_path}")
159
+ sys.exit(1)
160
+
161
+ problem_files = scan_directory(scan_path, fix=args.fix, dry_run=args.dry_run)
162
+
163
+ print("\n" + "="*60)
164
+ if problem_files:
165
+ print(f"❌ Found {len(problem_files)} file(s) with encoding issues:")
166
+ for f in problem_files:
167
+ print(f" - {f}")
168
+
169
+ if not args.fix and not args.dry_run:
170
+ print("\nRun with --fix to attempt automatic conversion")
171
+ print("Run with --dry-run to preview changes")
172
+
173
+ sys.exit(1)
174
+ else:
175
+ print("✅ All markdown files have valid UTF-8 encoding!")
176
+ sys.exit(0)
177
+
178
+
179
+ if __name__ == '__main__':
180
+ main()
@@ -0,0 +1,274 @@
1
+ """Task metadata validation and repair for Spec Kitty.
2
+
3
+ Detects and fixes inconsistencies between work package file locations
4
+ and their frontmatter metadata.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import yaml
15
+
16
+ from specify_cli.template import parse_frontmatter
17
+ from specify_cli.tasks_support import build_document
18
+
19
+ __all__ = [
20
+ "TaskMetadataError",
21
+ "detect_lane_mismatch",
22
+ "repair_lane_mismatch",
23
+ "validate_task_metadata",
24
+ "scan_all_tasks_for_mismatches",
25
+ ]
26
+
27
+
28
+ class TaskMetadataError(Exception):
29
+ """Raised when task metadata is inconsistent."""
30
+
31
+ pass
32
+
33
+
34
+ def detect_lane_mismatch(task_file: Path) -> tuple[bool, Optional[str], Optional[str]]:
35
+ """Detect if task file's lane metadata doesn't match its directory.
36
+
37
+ Args:
38
+ task_file: Path to the work package prompt file
39
+
40
+ Returns:
41
+ Tuple of (has_mismatch, expected_lane, actual_lane)
42
+ - has_mismatch: True if lane doesn't match directory
43
+ - expected_lane: Lane based on file location (e.g., "for_review")
44
+ - actual_lane: Lane from frontmatter metadata
45
+
46
+ Examples:
47
+ >>> task_file = Path("tasks/for_review/WP01.md")
48
+ >>> has_mismatch, expected, actual = detect_lane_mismatch(task_file)
49
+ >>> if has_mismatch:
50
+ ... print(f"File in {expected} but metadata says {actual}")
51
+ """
52
+ if not task_file.exists():
53
+ return False, None, None
54
+
55
+ # Determine expected lane from file path
56
+ expected_lane = None
57
+ for lane in ["planned", "doing", "for_review", "done"]:
58
+ if f"/tasks/{lane}/" in str(task_file) or f"\\tasks\\{lane}\\" in str(task_file):
59
+ expected_lane = lane
60
+ break
61
+
62
+ if not expected_lane:
63
+ # File not in a recognized lane directory
64
+ return False, None, None
65
+
66
+ # Read frontmatter
67
+ try:
68
+ content = task_file.read_text(encoding="utf-8-sig")
69
+ frontmatter, _, _ = parse_frontmatter(content)
70
+ except Exception:
71
+ return False, expected_lane, None
72
+
73
+ actual_lane = frontmatter.get("lane", "").strip()
74
+
75
+ has_mismatch = actual_lane != expected_lane
76
+ return has_mismatch, expected_lane, actual_lane
77
+
78
+
79
+ def repair_lane_mismatch(
80
+ task_file: Path,
81
+ *,
82
+ agent: str = "system",
83
+ shell_pid: str = "",
84
+ add_history: bool = True,
85
+ dry_run: bool = False,
86
+ ) -> tuple[bool, Optional[str]]:
87
+ """Repair lane mismatch by updating frontmatter to match directory.
88
+
89
+ Args:
90
+ task_file: Path to the work package prompt file
91
+ agent: Agent name for activity log
92
+ shell_pid: Shell PID for activity log
93
+ add_history: If True, append activity log entry
94
+ dry_run: If True, don't modify file
95
+
96
+ Returns:
97
+ Tuple of (was_repaired, error_message)
98
+ - was_repaired: True if repair was needed and applied
99
+ - error_message: None if successful, error description if failed
100
+
101
+ Examples:
102
+ >>> was_repaired, error = repair_lane_mismatch(
103
+ ... Path("tasks/for_review/WP01.md"),
104
+ ... agent="codex",
105
+ ... shell_pid="12345"
106
+ ... )
107
+ >>> if was_repaired:
108
+ ... print("Fixed lane metadata")
109
+ """
110
+ has_mismatch, expected_lane, actual_lane = detect_lane_mismatch(task_file)
111
+
112
+ if not has_mismatch:
113
+ return False, None # No repair needed
114
+
115
+ if expected_lane is None:
116
+ return False, f"Could not determine expected lane for {task_file.name}"
117
+
118
+ try:
119
+ content = task_file.read_text(encoding="utf-8-sig")
120
+ frontmatter, body, padding = parse_frontmatter(content)
121
+ except Exception as exc:
122
+ return False, f"Failed to parse frontmatter: {exc}"
123
+
124
+ # Update lane in frontmatter
125
+ frontmatter["lane"] = expected_lane
126
+
127
+ # Add activity log entry if requested
128
+ if add_history:
129
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
130
+ history_entry = (
131
+ f" - timestamp: \"{timestamp}\"\n"
132
+ f" lane: \"{expected_lane}\"\n"
133
+ f" agent: \"{agent}\"\n"
134
+ f" shell_pid: \"{shell_pid}\"\n"
135
+ f" action: \"Auto-repaired lane metadata (was: {actual_lane})\"\n"
136
+ )
137
+
138
+ # Find activity_log in frontmatter
139
+ if "activity_log" in frontmatter:
140
+ # Append to existing activity log
141
+ existing_log = frontmatter.get("activity_log", "")
142
+ if isinstance(existing_log, list):
143
+ # Already parsed as list - append dict
144
+ frontmatter["activity_log"].append({
145
+ "timestamp": timestamp,
146
+ "lane": expected_lane,
147
+ "agent": agent,
148
+ "shell_pid": shell_pid,
149
+ "action": f"Auto-repaired lane metadata (was: {actual_lane})"
150
+ })
151
+ elif isinstance(existing_log, str):
152
+ # Raw YAML string - append entry
153
+ frontmatter["activity_log"] = existing_log.rstrip() + "\n" + history_entry
154
+ else:
155
+ # Create new activity log
156
+ frontmatter["activity_log"] = history_entry
157
+
158
+ if dry_run:
159
+ return True, None # Would repair but dry run
160
+
161
+ # Rebuild file content
162
+ try:
163
+ # Convert frontmatter dict back to YAML string
164
+ frontmatter_yaml = yaml.dump(frontmatter, default_flow_style=False, allow_unicode=True, sort_keys=False)
165
+ new_content = build_document(frontmatter_yaml, body, padding)
166
+ task_file.write_text(new_content, encoding="utf-8-sig")
167
+ return True, None
168
+ except Exception as exc:
169
+ return False, f"Failed to write file: {exc}"
170
+
171
+
172
+ def validate_task_metadata(task_file: Path) -> list[str]:
173
+ """Validate task metadata and return list of issues.
174
+
175
+ Args:
176
+ task_file: Path to the work package prompt file
177
+
178
+ Returns:
179
+ List of validation issues (empty if valid)
180
+
181
+ Issues checked:
182
+ - Lane mismatch between directory and frontmatter
183
+ - Missing required frontmatter fields
184
+ - Invalid lane values
185
+ - Malformed activity log
186
+
187
+ Examples:
188
+ >>> issues = validate_task_metadata(Path("tasks/doing/WP01.md"))
189
+ >>> if issues:
190
+ ... for issue in issues:
191
+ ... print(f"⚠️ {issue}")
192
+ """
193
+ issues = []
194
+
195
+ if not task_file.exists():
196
+ issues.append(f"File not found: {task_file}")
197
+ return issues
198
+
199
+ # Check lane mismatch
200
+ has_mismatch, expected_lane, actual_lane = detect_lane_mismatch(task_file)
201
+ if has_mismatch:
202
+ issues.append(
203
+ f"Lane mismatch: file in '{expected_lane}/' but metadata says '{actual_lane}'"
204
+ )
205
+
206
+ # Parse frontmatter
207
+ try:
208
+ content = task_file.read_text(encoding="utf-8-sig")
209
+ frontmatter, _, _ = parse_frontmatter(content)
210
+ except Exception as exc:
211
+ issues.append(f"Failed to parse frontmatter: {exc}")
212
+ return issues
213
+
214
+ # Check required fields
215
+ required_fields = ["work_package_id", "lane"]
216
+ for field in required_fields:
217
+ if field not in frontmatter or not frontmatter[field]:
218
+ issues.append(f"Missing required field: {field}")
219
+
220
+ # Validate lane value
221
+ lane = frontmatter.get("lane", "")
222
+ valid_lanes = ["planned", "doing", "for_review", "done"]
223
+ if lane and lane not in valid_lanes:
224
+ issues.append(f"Invalid lane value: '{lane}' (must be one of {valid_lanes})")
225
+
226
+ # Check work_package_id format
227
+ wp_id = frontmatter.get("work_package_id", "")
228
+ if wp_id and not re.match(r"^WP\d+$", wp_id):
229
+ issues.append(f"Invalid work_package_id format: '{wp_id}' (should be WP##)")
230
+
231
+ return issues
232
+
233
+
234
+ def scan_all_tasks_for_mismatches(
235
+ feature_dir: Path,
236
+ ) -> dict[str, tuple[bool, Optional[str], Optional[str]]]:
237
+ """Scan all task files in a feature for lane mismatches.
238
+
239
+ Args:
240
+ feature_dir: Path to feature directory (e.g., kitty-specs/001-feature)
241
+
242
+ Returns:
243
+ Dictionary mapping file paths to (has_mismatch, expected_lane, actual_lane)
244
+ Only includes files with mismatches.
245
+
246
+ Examples:
247
+ >>> feature_dir = Path("kitty-specs/001-my-feature")
248
+ >>> mismatches = scan_all_tasks_for_mismatches(feature_dir)
249
+ >>> for file_path, (_, expected, actual) in mismatches.items():
250
+ ... print(f"{file_path}: {actual} → {expected}")
251
+ """
252
+ tasks_dir = feature_dir / "tasks"
253
+ if not tasks_dir.exists():
254
+ return {}
255
+
256
+ mismatches = {}
257
+
258
+ # Scan all lanes
259
+ for lane in ["planned", "doing", "for_review", "done"]:
260
+ lane_dir = tasks_dir / lane
261
+ if not lane_dir.exists():
262
+ continue
263
+
264
+ for task_file in lane_dir.rglob("WP*.md"):
265
+ has_mismatch, expected, actual = detect_lane_mismatch(task_file)
266
+ if has_mismatch:
267
+ # Store relative path for readability
268
+ try:
269
+ rel_path = task_file.relative_to(feature_dir)
270
+ except ValueError:
271
+ rel_path = task_file
272
+ mismatches[str(rel_path)] = (has_mismatch, expected, actual)
273
+
274
+ return mismatches