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,390 @@
|
|
|
1
|
+
"""Feature-centric dashboard handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from ..scanner import (
|
|
11
|
+
format_path_for_display,
|
|
12
|
+
resolve_feature_dir,
|
|
13
|
+
scan_all_features,
|
|
14
|
+
scan_feature_kanban,
|
|
15
|
+
)
|
|
16
|
+
from .base import DashboardHandler
|
|
17
|
+
from specify_cli.legacy_detector import is_legacy_format
|
|
18
|
+
from specify_cli.mission import MissionError, get_mission_by_name
|
|
19
|
+
|
|
20
|
+
__all__ = ["FeatureHandler"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FeatureHandler(DashboardHandler):
|
|
24
|
+
"""Serve feature lists, kanban lanes, and artifact viewers."""
|
|
25
|
+
|
|
26
|
+
def handle_features_list(self) -> None:
|
|
27
|
+
"""Return summary data for all features."""
|
|
28
|
+
self.send_response(200)
|
|
29
|
+
self.send_header('Content-type', 'application/json')
|
|
30
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
31
|
+
self.end_headers()
|
|
32
|
+
|
|
33
|
+
project_path = Path(self.project_dir).resolve()
|
|
34
|
+
features = scan_all_features(project_path)
|
|
35
|
+
|
|
36
|
+
# Add legacy format indicator to each feature
|
|
37
|
+
for feature in features:
|
|
38
|
+
feature_dir = project_path / feature['path']
|
|
39
|
+
feature['is_legacy'] = is_legacy_format(feature_dir)
|
|
40
|
+
|
|
41
|
+
# Derive active mission from the most active feature (per-feature mission model)
|
|
42
|
+
# Priority: feature with WPs in doing > for_review > most recent feature
|
|
43
|
+
mission_context = {
|
|
44
|
+
'name': 'No active feature',
|
|
45
|
+
'domain': 'unknown',
|
|
46
|
+
'version': '',
|
|
47
|
+
'slug': '',
|
|
48
|
+
'description': '',
|
|
49
|
+
'path': '',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
active_feature = None
|
|
53
|
+
for feature in features:
|
|
54
|
+
stats = feature.get('kanban_stats', {})
|
|
55
|
+
if stats.get('doing', 0) > 0:
|
|
56
|
+
active_feature = feature
|
|
57
|
+
break
|
|
58
|
+
if stats.get('for_review', 0) > 0 and active_feature is None:
|
|
59
|
+
active_feature = feature
|
|
60
|
+
|
|
61
|
+
# Fall back to most recent feature if none are active
|
|
62
|
+
if active_feature is None and features:
|
|
63
|
+
active_feature = features[0] # Already sorted by id descending (most recent first)
|
|
64
|
+
|
|
65
|
+
if active_feature:
|
|
66
|
+
feature_mission_key = active_feature.get('meta', {}).get('mission', 'software-dev')
|
|
67
|
+
try:
|
|
68
|
+
kittify_dir = project_path / ".kittify"
|
|
69
|
+
mission = get_mission_by_name(feature_mission_key, kittify_dir)
|
|
70
|
+
mission_context = {
|
|
71
|
+
'name': mission.name,
|
|
72
|
+
'domain': mission.config.domain,
|
|
73
|
+
'version': mission.config.version,
|
|
74
|
+
'slug': mission.path.name,
|
|
75
|
+
'description': mission.config.description or '',
|
|
76
|
+
'path': format_path_for_display(str(mission.path)),
|
|
77
|
+
'feature': active_feature.get('name', ''),
|
|
78
|
+
}
|
|
79
|
+
except MissionError:
|
|
80
|
+
# Fallback: show feature name with unknown mission
|
|
81
|
+
mission_context = {
|
|
82
|
+
'name': f"Unknown ({feature_mission_key})",
|
|
83
|
+
'domain': 'unknown',
|
|
84
|
+
'version': '',
|
|
85
|
+
'slug': feature_mission_key,
|
|
86
|
+
'description': '',
|
|
87
|
+
'path': '',
|
|
88
|
+
'feature': active_feature.get('name', ''),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
worktrees_root_path = project_path / '.worktrees'
|
|
92
|
+
try:
|
|
93
|
+
worktrees_root_resolved = worktrees_root_path.resolve()
|
|
94
|
+
except Exception:
|
|
95
|
+
worktrees_root_resolved = worktrees_root_path
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
current_path = Path.cwd().resolve()
|
|
99
|
+
except Exception:
|
|
100
|
+
current_path = Path.cwd()
|
|
101
|
+
|
|
102
|
+
worktrees_root_exists = worktrees_root_path.exists()
|
|
103
|
+
worktrees_root_display = (
|
|
104
|
+
format_path_for_display(str(worktrees_root_resolved))
|
|
105
|
+
if worktrees_root_exists
|
|
106
|
+
else None
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
active_worktree_display: Optional[str] = None
|
|
110
|
+
if worktrees_root_exists:
|
|
111
|
+
try:
|
|
112
|
+
current_path.relative_to(worktrees_root_resolved)
|
|
113
|
+
active_worktree_display = format_path_for_display(str(current_path))
|
|
114
|
+
except ValueError:
|
|
115
|
+
active_worktree_display = None
|
|
116
|
+
|
|
117
|
+
if not active_worktree_display and current_path != project_path:
|
|
118
|
+
active_worktree_display = format_path_for_display(str(current_path))
|
|
119
|
+
|
|
120
|
+
response = {
|
|
121
|
+
'features': features,
|
|
122
|
+
'project_path': format_path_for_display(str(project_path)),
|
|
123
|
+
'worktrees_root': worktrees_root_display,
|
|
124
|
+
'active_worktree': active_worktree_display,
|
|
125
|
+
'active_mission': mission_context,
|
|
126
|
+
}
|
|
127
|
+
self.wfile.write(json.dumps(response).encode())
|
|
128
|
+
|
|
129
|
+
def handle_kanban(self, path: str) -> None:
|
|
130
|
+
"""Return kanban data for a specific feature slug."""
|
|
131
|
+
parts = path.split('/')
|
|
132
|
+
if len(parts) >= 4:
|
|
133
|
+
feature_id = parts[3]
|
|
134
|
+
project_path = Path(self.project_dir).resolve()
|
|
135
|
+
kanban_data = scan_feature_kanban(project_path, feature_id)
|
|
136
|
+
|
|
137
|
+
# Check if feature uses legacy format
|
|
138
|
+
feature_dir = resolve_feature_dir(project_path, feature_id)
|
|
139
|
+
is_legacy = is_legacy_format(feature_dir) if feature_dir else False
|
|
140
|
+
|
|
141
|
+
response = {
|
|
142
|
+
'lanes': kanban_data,
|
|
143
|
+
'is_legacy': is_legacy,
|
|
144
|
+
'upgrade_needed': is_legacy,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
self.send_response(200)
|
|
148
|
+
self.send_header('Content-type', 'application/json')
|
|
149
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
150
|
+
self.end_headers()
|
|
151
|
+
self.wfile.write(json.dumps(response).encode())
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
self.send_response(404)
|
|
155
|
+
self.end_headers()
|
|
156
|
+
|
|
157
|
+
def handle_research(self, path: str) -> None:
|
|
158
|
+
"""Return research.md contents + artifacts, or serve a specific file."""
|
|
159
|
+
parts = path.split('/')
|
|
160
|
+
if len(parts) < 4:
|
|
161
|
+
self.send_response(404)
|
|
162
|
+
self.end_headers()
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
feature_id = parts[3]
|
|
166
|
+
project_path = Path(self.project_dir)
|
|
167
|
+
feature_dir = resolve_feature_dir(project_path, feature_id)
|
|
168
|
+
|
|
169
|
+
if len(parts) == 4:
|
|
170
|
+
response = {'main_file': None, 'artifacts': []}
|
|
171
|
+
|
|
172
|
+
if feature_dir:
|
|
173
|
+
research_md = feature_dir / 'research.md'
|
|
174
|
+
if research_md.exists():
|
|
175
|
+
try:
|
|
176
|
+
response['main_file'] = research_md.read_text(encoding='utf-8')
|
|
177
|
+
except UnicodeDecodeError as err:
|
|
178
|
+
error_msg = (
|
|
179
|
+
f'⚠️ **Encoding Error in research.md**\\n\\n'
|
|
180
|
+
f'This file contains non-UTF-8 characters at position {err.start}.\\n'
|
|
181
|
+
'Please convert the file to UTF-8 encoding.\\n\\n'
|
|
182
|
+
'Attempting to read with error recovery...\\n\\n---\\n\\n'
|
|
183
|
+
)
|
|
184
|
+
response['main_file'] = error_msg + research_md.read_text(
|
|
185
|
+
encoding='utf-8', errors='replace'
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
research_dir = feature_dir / 'research'
|
|
189
|
+
if research_dir.exists() and research_dir.is_dir():
|
|
190
|
+
for file_path in sorted(research_dir.rglob('*')):
|
|
191
|
+
if file_path.is_file():
|
|
192
|
+
relative_path = str(file_path.relative_to(feature_dir))
|
|
193
|
+
icon = '📄'
|
|
194
|
+
if file_path.suffix == '.csv':
|
|
195
|
+
icon = '📊'
|
|
196
|
+
elif file_path.suffix == '.md':
|
|
197
|
+
icon = '📝'
|
|
198
|
+
elif file_path.suffix in ['.xlsx', '.xls']:
|
|
199
|
+
icon = '📈'
|
|
200
|
+
elif file_path.suffix == '.json':
|
|
201
|
+
icon = '📋'
|
|
202
|
+
response['artifacts'].append({
|
|
203
|
+
'name': file_path.name,
|
|
204
|
+
'path': relative_path,
|
|
205
|
+
'icon': icon,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
self.send_response(200)
|
|
209
|
+
self.send_header('Content-type', 'application/json')
|
|
210
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
211
|
+
self.end_headers()
|
|
212
|
+
self.wfile.write(json.dumps(response).encode())
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if len(parts) >= 5 and feature_dir:
|
|
216
|
+
file_path_encoded = parts[4]
|
|
217
|
+
file_path_str = urllib.parse.unquote(file_path_encoded)
|
|
218
|
+
artifact_file = (feature_dir / file_path_str).resolve()
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
artifact_file.relative_to(feature_dir.resolve())
|
|
222
|
+
except ValueError:
|
|
223
|
+
self.send_response(404)
|
|
224
|
+
self.end_headers()
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
if artifact_file.exists() and artifact_file.is_file():
|
|
228
|
+
self.send_response(200)
|
|
229
|
+
self.send_header('Content-type', 'text/plain')
|
|
230
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
231
|
+
self.end_headers()
|
|
232
|
+
try:
|
|
233
|
+
content = artifact_file.read_text(encoding='utf-8')
|
|
234
|
+
self.wfile.write(content.encode('utf-8'))
|
|
235
|
+
except UnicodeDecodeError as err:
|
|
236
|
+
error_msg = (
|
|
237
|
+
f'⚠️ Encoding Error in {artifact_file.name}\\n\\n'
|
|
238
|
+
f'This file contains non-UTF-8 characters at position {err.start}.\\n'
|
|
239
|
+
'Please convert the file to UTF-8 encoding.\\n\\n'
|
|
240
|
+
'Attempting to read with error recovery...\\n\\n'
|
|
241
|
+
)
|
|
242
|
+
content = artifact_file.read_text(encoding='utf-8', errors='replace')
|
|
243
|
+
self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
self.wfile.write(f'Error reading file: {exc}'.encode('utf-8'))
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
self.send_response(404)
|
|
249
|
+
self.end_headers()
|
|
250
|
+
|
|
251
|
+
def _handle_artifact_directory(self, path: str, directory_name: str, md_icon: str = '📝') -> None:
|
|
252
|
+
"""Generic handler for artifact directories (contracts, checklists, etc).
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
path: The request path
|
|
256
|
+
directory_name: Name of the subdirectory (e.g., 'contracts', 'checklists')
|
|
257
|
+
md_icon: Icon to use for .md files (default: '📝')
|
|
258
|
+
"""
|
|
259
|
+
parts = path.split('/')
|
|
260
|
+
if len(parts) < 4:
|
|
261
|
+
self.send_response(404)
|
|
262
|
+
self.end_headers()
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
feature_id = parts[3]
|
|
266
|
+
project_path = Path(self.project_dir)
|
|
267
|
+
feature_dir = resolve_feature_dir(project_path, feature_id)
|
|
268
|
+
|
|
269
|
+
if len(parts) == 4:
|
|
270
|
+
# Return directory listing
|
|
271
|
+
response = {'files': []}
|
|
272
|
+
|
|
273
|
+
if feature_dir:
|
|
274
|
+
artifact_dir = feature_dir / directory_name
|
|
275
|
+
if artifact_dir.exists() and artifact_dir.is_dir():
|
|
276
|
+
for file_path in sorted(artifact_dir.rglob('*')):
|
|
277
|
+
if file_path.is_file():
|
|
278
|
+
relative_path = str(file_path.relative_to(feature_dir))
|
|
279
|
+
icon = '📄'
|
|
280
|
+
if file_path.suffix == '.md':
|
|
281
|
+
icon = md_icon
|
|
282
|
+
elif file_path.suffix == '.json':
|
|
283
|
+
icon = '📋'
|
|
284
|
+
response['files'].append({
|
|
285
|
+
'name': file_path.name,
|
|
286
|
+
'path': relative_path,
|
|
287
|
+
'icon': icon,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
self.send_response(200)
|
|
291
|
+
self.send_header('Content-type', 'application/json')
|
|
292
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
293
|
+
self.end_headers()
|
|
294
|
+
self.wfile.write(json.dumps(response).encode())
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if len(parts) >= 5 and feature_dir:
|
|
298
|
+
# Serve specific file
|
|
299
|
+
file_path_encoded = parts[4]
|
|
300
|
+
file_path_str = urllib.parse.unquote(file_path_encoded)
|
|
301
|
+
artifact_file = (feature_dir / file_path_str).resolve()
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
artifact_file.relative_to(feature_dir.resolve())
|
|
305
|
+
except ValueError:
|
|
306
|
+
self.send_response(404)
|
|
307
|
+
self.end_headers()
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if artifact_file.exists() and artifact_file.is_file():
|
|
311
|
+
self.send_response(200)
|
|
312
|
+
self.send_header('Content-type', 'text/plain')
|
|
313
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
314
|
+
self.end_headers()
|
|
315
|
+
try:
|
|
316
|
+
content = artifact_file.read_text(encoding='utf-8')
|
|
317
|
+
self.wfile.write(content.encode('utf-8'))
|
|
318
|
+
except UnicodeDecodeError as err:
|
|
319
|
+
error_msg = (
|
|
320
|
+
f'⚠️ Encoding Error in {artifact_file.name}\\n\\n'
|
|
321
|
+
f'This file contains non-UTF-8 characters at position {err.start}.\\n'
|
|
322
|
+
'Please convert the file to UTF-8 encoding.\\n\\n'
|
|
323
|
+
'Attempting to read with error recovery...\\n\\n'
|
|
324
|
+
)
|
|
325
|
+
content = artifact_file.read_text(encoding='utf-8', errors='replace')
|
|
326
|
+
self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
|
|
327
|
+
except Exception as exc:
|
|
328
|
+
self.wfile.write(f'Error reading file: {exc}'.encode('utf-8'))
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
self.send_response(404)
|
|
332
|
+
self.end_headers()
|
|
333
|
+
|
|
334
|
+
def handle_contracts(self, path: str) -> None:
|
|
335
|
+
"""Return contracts directory listing or serve a specific file."""
|
|
336
|
+
self._handle_artifact_directory(path, 'contracts', md_icon='📝')
|
|
337
|
+
|
|
338
|
+
def handle_checklists(self, path: str) -> None:
|
|
339
|
+
"""Return checklists directory listing or serve a specific file."""
|
|
340
|
+
self._handle_artifact_directory(path, 'checklists', md_icon='✅')
|
|
341
|
+
|
|
342
|
+
def handle_artifact(self, path: str) -> None:
|
|
343
|
+
"""Serve primary artifacts like spec.md and plan.md."""
|
|
344
|
+
parts = path.split('/')
|
|
345
|
+
if len(parts) < 4:
|
|
346
|
+
self.send_response(404)
|
|
347
|
+
self.end_headers()
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
feature_id = parts[3]
|
|
351
|
+
artifact_name = parts[4] if len(parts) > 4 else ''
|
|
352
|
+
|
|
353
|
+
project_path = Path(self.project_dir)
|
|
354
|
+
feature_dir = resolve_feature_dir(project_path, feature_id)
|
|
355
|
+
|
|
356
|
+
artifact_map = {
|
|
357
|
+
'spec': 'spec.md',
|
|
358
|
+
'plan': 'plan.md',
|
|
359
|
+
'tasks': 'tasks.md',
|
|
360
|
+
'research': 'research.md',
|
|
361
|
+
'quickstart': 'quickstart.md',
|
|
362
|
+
'data-model': 'data-model.md',
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
filename = artifact_map.get(artifact_name)
|
|
366
|
+
if feature_dir and filename:
|
|
367
|
+
artifact_file = feature_dir / filename
|
|
368
|
+
if artifact_file.exists():
|
|
369
|
+
self.send_response(200)
|
|
370
|
+
self.send_header('Content-type', 'text/plain')
|
|
371
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
372
|
+
self.end_headers()
|
|
373
|
+
try:
|
|
374
|
+
content = artifact_file.read_text(encoding='utf-8')
|
|
375
|
+
self.wfile.write(content.encode('utf-8'))
|
|
376
|
+
except UnicodeDecodeError as err:
|
|
377
|
+
error_msg = (
|
|
378
|
+
f'⚠️ **Encoding Error in {filename}**\\n\\n'
|
|
379
|
+
f'This file contains non-UTF-8 characters at position {err.start}.\\n'
|
|
380
|
+
'Please convert the file to UTF-8 encoding.\\n\\n'
|
|
381
|
+
'Attempting to read with error recovery...\\n\\n---\\n\\n'
|
|
382
|
+
)
|
|
383
|
+
content = artifact_file.read_text(encoding='utf-8', errors='replace')
|
|
384
|
+
self.wfile.write(error_msg.encode('utf-8') + content.encode('utf-8'))
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
self.wfile.write(f'Error reading {filename}: {exc}'.encode('utf-8'))
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
self.send_response(404)
|
|
390
|
+
self.end_headers()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Router that dispatches HTTP requests to specialized handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
from .api import APIHandler
|
|
8
|
+
from .features import FeatureHandler
|
|
9
|
+
from .static import STATIC_URL_PREFIX, StaticHandler
|
|
10
|
+
|
|
11
|
+
__all__ = ["DashboardRouter"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DashboardRouter(APIHandler, FeatureHandler, StaticHandler):
|
|
15
|
+
"""Dispatch GET/POST requests to API, feature, or static handlers."""
|
|
16
|
+
|
|
17
|
+
def do_POST(self) -> None: # noqa: N802 (BaseHTTPRequestHandler signature)
|
|
18
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
19
|
+
path = parsed_path.path
|
|
20
|
+
|
|
21
|
+
if path == '/api/shutdown':
|
|
22
|
+
self.handle_shutdown()
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
self.send_response(404)
|
|
26
|
+
self.end_headers()
|
|
27
|
+
|
|
28
|
+
def do_GET(self) -> None: # noqa: N802
|
|
29
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
30
|
+
path = parsed_path.path
|
|
31
|
+
|
|
32
|
+
if path == '/':
|
|
33
|
+
self.handle_root()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if path == '/api/health':
|
|
37
|
+
self.handle_health()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if path == '/api/shutdown':
|
|
41
|
+
self.handle_shutdown()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if path == '/api/features':
|
|
45
|
+
self.handle_features_list()
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if path.startswith('/api/kanban/'):
|
|
49
|
+
self.handle_kanban(path)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if path.startswith('/api/research/'):
|
|
53
|
+
self.handle_research(path)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if path.startswith('/api/contracts/'):
|
|
57
|
+
self.handle_contracts(path)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if path.startswith('/api/checklists/'):
|
|
61
|
+
self.handle_checklists(path)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if path.startswith('/api/artifact/'):
|
|
65
|
+
self.handle_artifact(path)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if path == '/api/diagnostics':
|
|
69
|
+
self.handle_diagnostics()
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if path == '/api/constitution':
|
|
73
|
+
self.handle_constitution()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if path.startswith(STATIC_URL_PREFIX):
|
|
77
|
+
self.handle_static(path)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self.send_response(404)
|
|
81
|
+
self.end_headers()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Static asset handler for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .base import DashboardHandler
|
|
9
|
+
|
|
10
|
+
STATIC_URL_PREFIX = '/static/'
|
|
11
|
+
STATIC_DIR = (Path(__file__).resolve().parents[1] / 'static').resolve()
|
|
12
|
+
|
|
13
|
+
__all__ = ["STATIC_DIR", "STATIC_URL_PREFIX", "StaticHandler"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StaticHandler(DashboardHandler):
|
|
17
|
+
"""Serve files from the dashboard/static directory."""
|
|
18
|
+
|
|
19
|
+
def handle_static(self, path: str) -> None:
|
|
20
|
+
relative_path = path[len(STATIC_URL_PREFIX):]
|
|
21
|
+
static_root = STATIC_DIR
|
|
22
|
+
try:
|
|
23
|
+
safe_path = (STATIC_DIR / relative_path).resolve()
|
|
24
|
+
except (RuntimeError, ValueError):
|
|
25
|
+
safe_path = None
|
|
26
|
+
|
|
27
|
+
if not relative_path or not safe_path:
|
|
28
|
+
self.send_response(404)
|
|
29
|
+
self.end_headers()
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
safe_path.relative_to(static_root)
|
|
34
|
+
except ValueError:
|
|
35
|
+
self.send_response(404)
|
|
36
|
+
self.end_headers()
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if not safe_path.is_file():
|
|
40
|
+
self.send_response(404)
|
|
41
|
+
self.end_headers()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
mime_type, _ = mimetypes.guess_type(safe_path.name)
|
|
45
|
+
self.send_response(200)
|
|
46
|
+
self.send_header('Content-type', mime_type or 'application/octet-stream')
|
|
47
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
48
|
+
self.end_headers()
|
|
49
|
+
with safe_path.open('rb') as static_file:
|
|
50
|
+
self.wfile.write(static_file.read())
|