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,384 @@
1
+ """Frontmatter management with absolute consistency enforcement.
2
+
3
+ This module provides the ONLY way to read and write YAML frontmatter
4
+ in spec-kitty markdown files. All frontmatter operations MUST go through
5
+ these functions to ensure absolute consistency.
6
+
7
+ LLMs and scripts should NEVER manually edit YAML frontmatter.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, Dict, Optional
16
+
17
+ from ruamel.yaml import YAML
18
+ from ruamel.yaml.comments import CommentedMap
19
+
20
+
21
+ class FrontmatterError(Exception):
22
+ """Error in frontmatter operations."""
23
+ pass
24
+
25
+
26
+ class FrontmatterManager:
27
+ """Manages YAML frontmatter with enforced consistency.
28
+
29
+ Rules:
30
+ 1. Always use ruamel.yaml for parsing/writing
31
+ 2. Never quote scalar values (let YAML decide)
32
+ 3. Use consistent indentation (2 spaces)
33
+ 4. Preserve comments
34
+ 5. Sort keys in consistent order
35
+ """
36
+
37
+ # Standard field order for work package frontmatter
38
+ WP_FIELD_ORDER = [
39
+ "work_package_id",
40
+ "title",
41
+ "lane",
42
+ "dependencies", # List of WP IDs this WP depends on (e.g., ['WP01', 'WP02'])
43
+ "base_branch", # Git branch this WP was created from (e.g., "010-feature-WP01" or "main")
44
+ "base_commit", # Git commit SHA this WP was created from (snapshot for validation)
45
+ "created_at", # ISO timestamp when workspace was created
46
+ "subtasks",
47
+ "phase",
48
+ "assignee",
49
+ "agent",
50
+ "shell_pid",
51
+ "review_status",
52
+ "reviewed_by",
53
+ "history",
54
+ ]
55
+
56
+ def __init__(self):
57
+ """Initialize with ruamel.yaml configured for consistency."""
58
+ self.yaml = YAML()
59
+ self.yaml.default_flow_style = False
60
+ self.yaml.preserve_quotes = False # Don't preserve quotes - let YAML decide
61
+ self.yaml.width = 4096 # Prevent line wrapping
62
+ self.yaml.indent(mapping=2, sequence=2, offset=0)
63
+
64
+ def read(self, file_path: Path) -> tuple[Dict[str, Any], str]:
65
+ """Read frontmatter and body from a markdown file.
66
+
67
+ Args:
68
+ file_path: Path to markdown file
69
+
70
+ Returns:
71
+ Tuple of (frontmatter_dict, body_text)
72
+
73
+ Raises:
74
+ FrontmatterError: If file has no frontmatter or is malformed
75
+ """
76
+ if not file_path.exists():
77
+ raise FrontmatterError(f"File not found: {file_path}")
78
+
79
+ content = file_path.read_text(encoding="utf-8-sig")
80
+
81
+ if not content.startswith("---"):
82
+ raise FrontmatterError(f"File has no frontmatter: {file_path}")
83
+
84
+ # Find closing ---
85
+ lines = content.split("\n")
86
+ closing_idx = -1
87
+ for i, line in enumerate(lines[1:], start=1):
88
+ if line.strip() == "---":
89
+ closing_idx = i
90
+ break
91
+
92
+ if closing_idx == -1:
93
+ raise FrontmatterError(f"Malformed frontmatter (no closing ---): {file_path}")
94
+
95
+ # Parse frontmatter
96
+ frontmatter_text = "\n".join(lines[1:closing_idx])
97
+ try:
98
+ frontmatter = self.yaml.load(frontmatter_text)
99
+ if frontmatter is None:
100
+ frontmatter = {}
101
+ except Exception as e:
102
+ raise FrontmatterError(f"Invalid YAML in {file_path}: {e}")
103
+
104
+ # Ensure dependencies field exists for WP files only (backward compatibility with pre-0.11.0)
105
+ if file_path.name.startswith("WP") and "dependencies" not in frontmatter:
106
+ frontmatter["dependencies"] = []
107
+
108
+ # Get body (everything after closing ---)
109
+ body = "\n".join(lines[closing_idx + 1:])
110
+
111
+ return frontmatter, body
112
+
113
+ def write(self, file_path: Path, frontmatter: Dict[str, Any], body: str) -> None:
114
+ """Write frontmatter and body to a markdown file.
115
+
116
+ Args:
117
+ file_path: Path to markdown file
118
+ frontmatter: Dictionary of frontmatter fields
119
+ body: Body text (everything after frontmatter)
120
+ """
121
+ # Normalize frontmatter (sort keys, clean values)
122
+ normalized = self._normalize_frontmatter(frontmatter)
123
+
124
+ # Write to string buffer first
125
+ import io
126
+ buffer = io.StringIO()
127
+ buffer.write("---\n")
128
+ self.yaml.dump(normalized, buffer)
129
+ buffer.write("---\n")
130
+ buffer.write(body)
131
+
132
+ # Write to file
133
+ file_path.write_text(buffer.getvalue(), encoding="utf-8")
134
+
135
+ def update_field(self, file_path: Path, field: str, value: Any) -> None:
136
+ """Update a single field in frontmatter.
137
+
138
+ Args:
139
+ file_path: Path to markdown file
140
+ field: Field name to update
141
+ value: New value for field
142
+ """
143
+ frontmatter, body = self.read(file_path)
144
+ frontmatter[field] = value
145
+ self.write(file_path, frontmatter, body)
146
+
147
+ def update_fields(self, file_path: Path, updates: Dict[str, Any]) -> None:
148
+ """Update multiple fields in frontmatter.
149
+
150
+ Args:
151
+ file_path: Path to markdown file
152
+ updates: Dictionary of field updates
153
+ """
154
+ frontmatter, body = self.read(file_path)
155
+ frontmatter.update(updates)
156
+ self.write(file_path, frontmatter, body)
157
+
158
+ def get_field(self, file_path: Path, field: str, default: Any = None) -> Any:
159
+ """Get a single field from frontmatter.
160
+
161
+ Args:
162
+ file_path: Path to markdown file
163
+ field: Field name to get
164
+ default: Default value if field doesn't exist
165
+
166
+ Returns:
167
+ Field value or default
168
+ """
169
+ frontmatter, _ = self.read(file_path)
170
+ return frontmatter.get(field, default)
171
+
172
+ def add_history_entry(
173
+ self,
174
+ file_path: Path,
175
+ action: str,
176
+ agent: Optional[str] = None,
177
+ note: Optional[str] = None
178
+ ) -> None:
179
+ """Add an entry to the history field.
180
+
181
+ Args:
182
+ file_path: Path to markdown file
183
+ action: Action description (e.g., "moved to for_review")
184
+ agent: Agent name (optional)
185
+ note: Additional note (optional)
186
+ """
187
+ frontmatter, body = self.read(file_path)
188
+
189
+ # Get or create history list
190
+ history = frontmatter.get("history", [])
191
+ if not isinstance(history, list):
192
+ history = []
193
+
194
+ # Create entry
195
+ entry = {
196
+ "timestamp": datetime.now().isoformat(),
197
+ "action": action,
198
+ }
199
+ if agent:
200
+ entry["agent"] = agent
201
+ if note:
202
+ entry["note"] = note
203
+
204
+ history.append(entry)
205
+ frontmatter["history"] = history
206
+
207
+ self.write(file_path, frontmatter, body)
208
+
209
+ def _normalize_frontmatter(self, frontmatter: Dict[str, Any]) -> CommentedMap:
210
+ """Normalize frontmatter for consistent output.
211
+
212
+ Args:
213
+ frontmatter: Raw frontmatter dictionary
214
+
215
+ Returns:
216
+ Normalized CommentedMap with consistent field order
217
+ """
218
+ # Create ordered map
219
+ normalized = CommentedMap()
220
+
221
+ # Add fields in standard order (if they exist)
222
+ for field in self.WP_FIELD_ORDER:
223
+ if field in frontmatter:
224
+ normalized[field] = frontmatter[field]
225
+
226
+ # Add any remaining fields (alphabetically)
227
+ remaining = sorted(set(frontmatter.keys()) - set(self.WP_FIELD_ORDER))
228
+ for field in remaining:
229
+ normalized[field] = frontmatter[field]
230
+
231
+ return normalized
232
+
233
+ def _validate_dependencies(self, dependencies: Any) -> list[str]:
234
+ """Validate dependencies field format.
235
+
236
+ Args:
237
+ dependencies: Dependencies value to validate
238
+
239
+ Returns:
240
+ List of validation errors (empty if valid)
241
+ """
242
+ errors = []
243
+
244
+ if not isinstance(dependencies, list):
245
+ errors.append(f"dependencies must be a list, got {type(dependencies).__name__}")
246
+ return errors
247
+
248
+ wp_pattern = re.compile(r'^WP\d{2}$')
249
+ seen = set()
250
+
251
+ for dep in dependencies:
252
+ if not isinstance(dep, str):
253
+ errors.append(f"Dependency must be string, got {type(dep).__name__}")
254
+ elif not wp_pattern.match(dep):
255
+ errors.append(f"Invalid WP ID format: {dep} (must be WP## like WP01, WP02)")
256
+ elif dep in seen:
257
+ errors.append(f"Duplicate dependency: {dep}")
258
+ else:
259
+ seen.add(dep)
260
+
261
+ return errors
262
+
263
+ def validate(self, file_path: Path) -> list[str]:
264
+ """Validate frontmatter consistency.
265
+
266
+ Args:
267
+ file_path: Path to markdown file
268
+
269
+ Returns:
270
+ List of validation errors (empty if valid)
271
+ """
272
+ errors = []
273
+
274
+ try:
275
+ frontmatter, _ = self.read(file_path)
276
+ except FrontmatterError as e:
277
+ return [str(e)]
278
+
279
+ # Check for required fields (work packages only)
280
+ if file_path.name.startswith("WP"):
281
+ required = ["work_package_id", "title", "lane"]
282
+ for field in required:
283
+ if field not in frontmatter:
284
+ errors.append(f"Missing required field: {field}")
285
+
286
+ # Validate lane value
287
+ if "lane" in frontmatter:
288
+ valid_lanes = ["planned", "doing", "for_review", "done"]
289
+ if frontmatter["lane"] not in valid_lanes:
290
+ errors.append(
291
+ f"Invalid lane value: {frontmatter['lane']} "
292
+ f"(must be one of: {', '.join(valid_lanes)})"
293
+ )
294
+
295
+ # Validate dependencies field (if present)
296
+ if "dependencies" in frontmatter:
297
+ dep_errors = self._validate_dependencies(frontmatter["dependencies"])
298
+ errors.extend(dep_errors)
299
+
300
+ return errors
301
+
302
+
303
+ # Global instance for convenience
304
+ _manager = FrontmatterManager()
305
+
306
+
307
+ # Convenience functions that use the global manager
308
+ def read_frontmatter(file_path: Path) -> tuple[Dict[str, Any], str]:
309
+ """Read frontmatter and body from a markdown file."""
310
+ return _manager.read(file_path)
311
+
312
+
313
+ def write_frontmatter(file_path: Path, frontmatter: Dict[str, Any], body: str) -> None:
314
+ """Write frontmatter and body to a markdown file."""
315
+ _manager.write(file_path, frontmatter, body)
316
+
317
+
318
+ def update_field(file_path: Path, field: str, value: Any) -> None:
319
+ """Update a single field in frontmatter."""
320
+ _manager.update_field(file_path, field, value)
321
+
322
+
323
+ def update_fields(file_path: Path, updates: Dict[str, Any]) -> None:
324
+ """Update multiple fields in frontmatter."""
325
+ _manager.update_fields(file_path, updates)
326
+
327
+
328
+ def get_field(file_path: Path, field: str, default: Any = None) -> Any:
329
+ """Get a single field from frontmatter."""
330
+ return _manager.get_field(file_path, field, default)
331
+
332
+
333
+ def add_history_entry(
334
+ file_path: Path,
335
+ action: str,
336
+ agent: Optional[str] = None,
337
+ note: Optional[str] = None
338
+ ) -> None:
339
+ """Add an entry to the history field."""
340
+ _manager.add_history_entry(file_path, action, agent, note)
341
+
342
+
343
+ def validate_frontmatter(file_path: Path) -> list[str]:
344
+ """Validate frontmatter consistency."""
345
+ return _manager.validate(file_path)
346
+
347
+
348
+ def normalize_file(file_path: Path) -> bool:
349
+ """Normalize an existing file's frontmatter.
350
+
351
+ Args:
352
+ file_path: Path to markdown file
353
+
354
+ Returns:
355
+ True if file was modified, False if already normalized
356
+ """
357
+ try:
358
+ # Read current content
359
+ original_content = file_path.read_text(encoding="utf-8-sig")
360
+
361
+ # Read and rewrite (which normalizes)
362
+ frontmatter, body = _manager.read(file_path)
363
+ _manager.write(file_path, frontmatter, body)
364
+
365
+ # Check if changed
366
+ new_content = file_path.read_text(encoding="utf-8-sig")
367
+ return original_content != new_content
368
+
369
+ except FrontmatterError:
370
+ return False
371
+
372
+
373
+ __all__ = [
374
+ "FrontmatterError",
375
+ "FrontmatterManager",
376
+ "read_frontmatter",
377
+ "write_frontmatter",
378
+ "update_field",
379
+ "update_fields",
380
+ "get_field",
381
+ "add_history_entry",
382
+ "validate_frontmatter",
383
+ "normalize_file",
384
+ ]