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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- 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
|