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,204 @@
|
|
|
1
|
+
"""Project diagnostics helpers for the dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
|
|
11
|
+
__all__ = ["run_diagnostics"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_specify_cli_on_path() -> None:
|
|
15
|
+
"""Ensure the repository root (src directory) is on sys.path for fallback imports."""
|
|
16
|
+
candidate = Path(__file__).resolve().parents[2] # .../src
|
|
17
|
+
if str(candidate) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(candidate))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_diagnostics(project_dir: Path) -> Dict[str, Any]:
|
|
22
|
+
"""Run comprehensive diagnostics on the project setup using enhanced verification."""
|
|
23
|
+
try:
|
|
24
|
+
from ..manifest import FileManifest, WorktreeStatus # type: ignore
|
|
25
|
+
from ..acceptance import detect_feature_slug, AcceptanceError
|
|
26
|
+
except (ImportError, ValueError):
|
|
27
|
+
try:
|
|
28
|
+
from specify_cli.manifest import FileManifest, WorktreeStatus # type: ignore
|
|
29
|
+
from specify_cli.acceptance import detect_feature_slug, AcceptanceError
|
|
30
|
+
except ImportError:
|
|
31
|
+
_ensure_specify_cli_on_path()
|
|
32
|
+
from specify_cli.manifest import FileManifest, WorktreeStatus # type: ignore
|
|
33
|
+
from specify_cli.acceptance import detect_feature_slug, AcceptanceError
|
|
34
|
+
|
|
35
|
+
kittify_dir = project_dir / ".kittify"
|
|
36
|
+
repo_root = project_dir
|
|
37
|
+
|
|
38
|
+
diagnostics: Dict[str, Any] = {
|
|
39
|
+
'project_path': str(project_dir),
|
|
40
|
+
'current_working_directory': str(Path.cwd()),
|
|
41
|
+
'git_branch': None,
|
|
42
|
+
'in_worktree': False,
|
|
43
|
+
'worktrees_exist': False,
|
|
44
|
+
'active_mission': None,
|
|
45
|
+
'file_integrity': {},
|
|
46
|
+
'worktree_overview': {},
|
|
47
|
+
'current_feature': {},
|
|
48
|
+
'all_features': [],
|
|
49
|
+
'dashboard_health': {},
|
|
50
|
+
'observations': [],
|
|
51
|
+
'issues': [],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
manifest = FileManifest(kittify_dir)
|
|
55
|
+
worktree_status = WorktreeStatus(repo_root)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
['git', 'branch', '--show-current'],
|
|
60
|
+
cwd=project_dir,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
check=True,
|
|
64
|
+
)
|
|
65
|
+
diagnostics['git_branch'] = result.stdout.strip()
|
|
66
|
+
except subprocess.CalledProcessError:
|
|
67
|
+
diagnostics['issues'].append('Could not detect git branch')
|
|
68
|
+
|
|
69
|
+
diagnostics['in_worktree'] = '.worktrees' in str(Path.cwd())
|
|
70
|
+
worktrees_dir = project_dir / '.worktrees'
|
|
71
|
+
diagnostics['worktrees_exist'] = worktrees_dir.exists()
|
|
72
|
+
diagnostics['active_mission'] = manifest.active_mission
|
|
73
|
+
|
|
74
|
+
file_check = manifest.check_files()
|
|
75
|
+
expected_files = manifest.get_expected_files()
|
|
76
|
+
|
|
77
|
+
total_expected = sum(len(files) for files in expected_files.values())
|
|
78
|
+
total_present = len(file_check["present"])
|
|
79
|
+
total_missing = len(file_check["missing"])
|
|
80
|
+
|
|
81
|
+
diagnostics['file_integrity'] = {
|
|
82
|
+
"total_expected": total_expected,
|
|
83
|
+
"total_present": total_present,
|
|
84
|
+
"total_missing": total_missing,
|
|
85
|
+
"missing_files": list(file_check["missing"].keys()) if file_check["missing"] else [],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
worktree_summary = worktree_status.get_worktree_summary()
|
|
89
|
+
diagnostics['worktree_overview'] = worktree_summary
|
|
90
|
+
|
|
91
|
+
diagnostics['all_features'] = []
|
|
92
|
+
for feature_slug in worktree_status.get_all_features():
|
|
93
|
+
feature_status = worktree_status.get_feature_status(feature_slug)
|
|
94
|
+
diagnostics['all_features'].append({
|
|
95
|
+
'name': feature_slug,
|
|
96
|
+
'state': feature_status['state'],
|
|
97
|
+
'branch_exists': feature_status['branch_exists'],
|
|
98
|
+
'branch_merged': feature_status['branch_merged'],
|
|
99
|
+
'worktree_exists': feature_status['worktree_exists'],
|
|
100
|
+
'worktree_path': feature_status['worktree_path'],
|
|
101
|
+
'artifacts_in_main': feature_status['artifacts_in_main'],
|
|
102
|
+
'artifacts_in_worktree': feature_status['artifacts_in_worktree'],
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
feature_slug = detect_feature_slug(repo_root, cwd=Path.cwd())
|
|
107
|
+
if feature_slug:
|
|
108
|
+
feature_status = worktree_status.get_feature_status(feature_slug.strip())
|
|
109
|
+
diagnostics['current_feature'] = {
|
|
110
|
+
'detected': True,
|
|
111
|
+
'name': feature_slug.strip(),
|
|
112
|
+
'state': feature_status['state'],
|
|
113
|
+
'branch_exists': feature_status['branch_exists'],
|
|
114
|
+
'branch_merged': feature_status['branch_merged'],
|
|
115
|
+
'worktree_exists': feature_status['worktree_exists'],
|
|
116
|
+
'worktree_path': feature_status['worktree_path'],
|
|
117
|
+
'artifacts_in_main': feature_status['artifacts_in_main'],
|
|
118
|
+
'artifacts_in_worktree': feature_status['artifacts_in_worktree'],
|
|
119
|
+
}
|
|
120
|
+
except (AcceptanceError, Exception) as exc: # type: ignore[misc]
|
|
121
|
+
diagnostics['current_feature'] = {
|
|
122
|
+
'detected': False,
|
|
123
|
+
'error': str(exc),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
observations = []
|
|
127
|
+
|
|
128
|
+
if diagnostics['git_branch'] == 'main' and diagnostics['in_worktree']:
|
|
129
|
+
observations.append("Unusual: In worktree but on main branch")
|
|
130
|
+
|
|
131
|
+
current_feature = diagnostics.get('current_feature') or {}
|
|
132
|
+
if current_feature.get('detected') and current_feature.get('state') == 'in_development':
|
|
133
|
+
if not current_feature.get('worktree_exists'):
|
|
134
|
+
observations.append(
|
|
135
|
+
f"Feature {current_feature.get('name')} has no worktree but has development artifacts"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if total_missing > 0:
|
|
139
|
+
observations.append(f"Mission integrity: {total_missing} expected files not found")
|
|
140
|
+
|
|
141
|
+
if worktree_summary.get('active_worktrees', 0) > 5:
|
|
142
|
+
observations.append(f"Multiple worktrees active: {worktree_summary['active_worktrees']}")
|
|
143
|
+
|
|
144
|
+
# Check dashboard health
|
|
145
|
+
dashboard_file = kittify_dir / '.dashboard'
|
|
146
|
+
dashboard_health = {
|
|
147
|
+
'metadata_exists': dashboard_file.exists(),
|
|
148
|
+
'can_start': None,
|
|
149
|
+
'startup_test': None,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if dashboard_file.exists():
|
|
153
|
+
try:
|
|
154
|
+
from ..dashboard.lifecycle import _parse_dashboard_file, _check_dashboard_health
|
|
155
|
+
url, port, token, pid = _parse_dashboard_file(dashboard_file)
|
|
156
|
+
dashboard_health['url'] = url
|
|
157
|
+
dashboard_health['port'] = port
|
|
158
|
+
dashboard_health['pid'] = pid
|
|
159
|
+
dashboard_health['has_pid'] = pid is not None
|
|
160
|
+
|
|
161
|
+
if port:
|
|
162
|
+
is_healthy = _check_dashboard_health(port, project_dir, token)
|
|
163
|
+
dashboard_health['responding'] = is_healthy
|
|
164
|
+
if not is_healthy:
|
|
165
|
+
diagnostics['issues'].append(f'Dashboard metadata exists but not responding on port {port}')
|
|
166
|
+
if pid:
|
|
167
|
+
# Check if process is alive
|
|
168
|
+
try:
|
|
169
|
+
from ..dashboard.lifecycle import _is_process_alive
|
|
170
|
+
if _is_process_alive(pid):
|
|
171
|
+
diagnostics['issues'].append(f'Dashboard process (PID {pid}) is alive but not responding')
|
|
172
|
+
else:
|
|
173
|
+
diagnostics['issues'].append(f'Dashboard process (PID {pid}) is dead - stale metadata file')
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
except Exception as e:
|
|
177
|
+
dashboard_health['parse_error'] = str(e)
|
|
178
|
+
diagnostics['issues'].append(f'Dashboard metadata file corrupted: {e}')
|
|
179
|
+
else:
|
|
180
|
+
# No dashboard running - try to start one and see what happens
|
|
181
|
+
try:
|
|
182
|
+
from ..dashboard.lifecycle import ensure_dashboard_running
|
|
183
|
+
url, port, started = ensure_dashboard_running(project_dir, background_process=False)
|
|
184
|
+
dashboard_health['can_start'] = True
|
|
185
|
+
dashboard_health['startup_test'] = 'SUCCESS'
|
|
186
|
+
dashboard_health['test_url'] = url
|
|
187
|
+
dashboard_health['test_port'] = port
|
|
188
|
+
|
|
189
|
+
# Stop the test dashboard
|
|
190
|
+
try:
|
|
191
|
+
from ..dashboard.lifecycle import stop_dashboard
|
|
192
|
+
stop_dashboard(project_dir)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
except Exception as e:
|
|
196
|
+
dashboard_health['can_start'] = False
|
|
197
|
+
dashboard_health['startup_test'] = 'FAILED'
|
|
198
|
+
dashboard_health['startup_error'] = str(e)
|
|
199
|
+
diagnostics['issues'].append(f'Dashboard cannot start: {e}')
|
|
200
|
+
|
|
201
|
+
diagnostics['dashboard_health'] = dashboard_health
|
|
202
|
+
diagnostics['observations'] = observations
|
|
203
|
+
|
|
204
|
+
return diagnostics
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Dashboard HTTP handler subpackage."""
|
|
2
|
+
|
|
3
|
+
from .api import APIHandler
|
|
4
|
+
from .base import DashboardHandler
|
|
5
|
+
from .features import FeatureHandler
|
|
6
|
+
from .router import DashboardRouter
|
|
7
|
+
from .static import STATIC_DIR, STATIC_URL_PREFIX, StaticHandler
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"APIHandler",
|
|
11
|
+
"DashboardHandler",
|
|
12
|
+
"DashboardRouter",
|
|
13
|
+
"FeatureHandler",
|
|
14
|
+
"StaticHandler",
|
|
15
|
+
"STATIC_DIR",
|
|
16
|
+
"STATIC_URL_PREFIX",
|
|
17
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""API-focused dashboard HTTP handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..diagnostics import run_diagnostics
|
|
9
|
+
from ..scanner import format_path_for_display, scan_all_features
|
|
10
|
+
from ..templates import get_dashboard_html
|
|
11
|
+
from .base import DashboardHandler
|
|
12
|
+
from specify_cli.mission import MissionError, get_mission_by_name
|
|
13
|
+
|
|
14
|
+
__all__ = ["APIHandler"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIHandler(DashboardHandler):
|
|
18
|
+
"""Serve dashboard root, health, diagnostics, and shutdown endpoints."""
|
|
19
|
+
|
|
20
|
+
def handle_root(self) -> None:
|
|
21
|
+
"""Return the rendered dashboard HTML shell."""
|
|
22
|
+
project_path = Path(self.project_dir).resolve()
|
|
23
|
+
|
|
24
|
+
# Derive active mission from the most active feature (per-feature mission model)
|
|
25
|
+
mission_context = {
|
|
26
|
+
'name': 'No active feature',
|
|
27
|
+
'domain': 'unknown',
|
|
28
|
+
'version': '',
|
|
29
|
+
'slug': '',
|
|
30
|
+
'description': '',
|
|
31
|
+
'path': '',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
features = scan_all_features(project_path)
|
|
36
|
+
|
|
37
|
+
# Find active feature: WPs in doing > for_review > most recent
|
|
38
|
+
active_feature = None
|
|
39
|
+
for feature in features:
|
|
40
|
+
stats = feature.get('kanban_stats', {})
|
|
41
|
+
if stats.get('doing', 0) > 0:
|
|
42
|
+
active_feature = feature
|
|
43
|
+
break
|
|
44
|
+
if stats.get('for_review', 0) > 0 and active_feature is None:
|
|
45
|
+
active_feature = feature
|
|
46
|
+
|
|
47
|
+
if active_feature is None and features:
|
|
48
|
+
active_feature = features[0] # Most recent
|
|
49
|
+
|
|
50
|
+
if active_feature:
|
|
51
|
+
feature_mission_key = active_feature.get('meta', {}).get('mission', 'software-dev')
|
|
52
|
+
kittify_dir = project_path / ".kittify"
|
|
53
|
+
mission = get_mission_by_name(feature_mission_key, kittify_dir)
|
|
54
|
+
mission_context = {
|
|
55
|
+
'name': mission.name,
|
|
56
|
+
'domain': mission.config.domain,
|
|
57
|
+
'version': mission.config.version,
|
|
58
|
+
'slug': mission.path.name,
|
|
59
|
+
'description': mission.config.description or '',
|
|
60
|
+
'path': format_path_for_display(str(mission.path)),
|
|
61
|
+
}
|
|
62
|
+
except (MissionError, Exception):
|
|
63
|
+
pass # Keep default "No active feature" context
|
|
64
|
+
|
|
65
|
+
self.send_response(200)
|
|
66
|
+
self.send_header('Content-type', 'text/html')
|
|
67
|
+
self.end_headers()
|
|
68
|
+
self.wfile.write(get_dashboard_html(mission_context=mission_context).encode())
|
|
69
|
+
|
|
70
|
+
def handle_health(self) -> None:
|
|
71
|
+
"""Return project health metadata."""
|
|
72
|
+
self.send_response(200)
|
|
73
|
+
self.send_header('Content-type', 'application/json')
|
|
74
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
75
|
+
self.end_headers()
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
project_path = str(Path(self.project_dir).resolve())
|
|
79
|
+
except Exception:
|
|
80
|
+
project_path = str(self.project_dir)
|
|
81
|
+
|
|
82
|
+
response_data = {
|
|
83
|
+
'status': 'ok',
|
|
84
|
+
'project_path': project_path,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
token = getattr(self, 'project_token', None)
|
|
88
|
+
if token:
|
|
89
|
+
response_data['token'] = token
|
|
90
|
+
|
|
91
|
+
self.wfile.write(json.dumps(response_data).encode())
|
|
92
|
+
|
|
93
|
+
def handle_shutdown(self) -> None:
|
|
94
|
+
"""Delegate to the shared shutdown helper."""
|
|
95
|
+
self._handle_shutdown()
|
|
96
|
+
|
|
97
|
+
def handle_diagnostics(self) -> None:
|
|
98
|
+
"""Run diagnostics and report JSON payloads (or errors)."""
|
|
99
|
+
try:
|
|
100
|
+
diagnostics = run_diagnostics(Path(self.project_dir))
|
|
101
|
+
self.send_response(200)
|
|
102
|
+
self.send_header('Content-type', 'application/json')
|
|
103
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
104
|
+
self.end_headers()
|
|
105
|
+
self.wfile.write(json.dumps(diagnostics).encode())
|
|
106
|
+
except Exception as exc: # pragma: no cover - fallback safety
|
|
107
|
+
import traceback
|
|
108
|
+
|
|
109
|
+
error_msg = {
|
|
110
|
+
"error": str(exc),
|
|
111
|
+
"traceback": traceback.format_exc(),
|
|
112
|
+
}
|
|
113
|
+
self.send_response(500)
|
|
114
|
+
self.send_header('Content-type', 'application/json')
|
|
115
|
+
self.end_headers()
|
|
116
|
+
self.wfile.write(json.dumps(error_msg).encode())
|
|
117
|
+
|
|
118
|
+
def handle_constitution(self) -> None:
|
|
119
|
+
"""Serve project-level constitution from .kittify/memory/constitution.md"""
|
|
120
|
+
try:
|
|
121
|
+
constitution_path = Path(self.project_dir) / ".kittify" / "memory" / "constitution.md"
|
|
122
|
+
|
|
123
|
+
if not constitution_path.exists():
|
|
124
|
+
self.send_response(404)
|
|
125
|
+
self.send_header('Content-type', 'text/plain')
|
|
126
|
+
self.end_headers()
|
|
127
|
+
self.wfile.write(b'Constitution not found')
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
content = constitution_path.read_text(encoding='utf-8')
|
|
131
|
+
self.send_response(200)
|
|
132
|
+
self.send_header('Content-type', 'text/plain; charset=utf-8')
|
|
133
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
134
|
+
self.end_headers()
|
|
135
|
+
self.wfile.write(content.encode('utf-8'))
|
|
136
|
+
except Exception as exc: # pragma: no cover - fallback safety
|
|
137
|
+
import traceback
|
|
138
|
+
|
|
139
|
+
error_msg = f"Error loading constitution: {exc}\n{traceback.format_exc()}"
|
|
140
|
+
self.send_response(500)
|
|
141
|
+
self.send_header('Content-type', 'text/plain')
|
|
142
|
+
self.end_headers()
|
|
143
|
+
self.wfile.write(error_msg.encode())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Shared helpers for dashboard HTTP handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from http.server import BaseHTTPRequestHandler
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
__all__ = ["DashboardHandler"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DashboardHandler(BaseHTTPRequestHandler):
|
|
16
|
+
"""Base class that provides shared helpers for router/endpoint handlers."""
|
|
17
|
+
|
|
18
|
+
project_dir: Optional[str] = None
|
|
19
|
+
project_token: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
def log_message(self, format: str, *args: Any) -> None: # noqa: A003 - signature from BaseHTTPRequestHandler
|
|
22
|
+
"""Suppress default HTTP handler logging noise."""
|
|
23
|
+
del format, args
|
|
24
|
+
|
|
25
|
+
def _send_json(self, status_code: int, payload: Dict[str, Any]) -> None:
|
|
26
|
+
"""Write a JSON response with common headers."""
|
|
27
|
+
self.send_response(status_code)
|
|
28
|
+
self.send_header('Content-type', 'application/json')
|
|
29
|
+
self.send_header('Cache-Control', 'no-cache')
|
|
30
|
+
self.end_headers()
|
|
31
|
+
self.wfile.write(json.dumps(payload).encode())
|
|
32
|
+
|
|
33
|
+
def _handle_shutdown(self) -> None:
|
|
34
|
+
"""Validate shutdown tokens and stop the server."""
|
|
35
|
+
expected_token = getattr(self, 'project_token', None)
|
|
36
|
+
|
|
37
|
+
token = None
|
|
38
|
+
if self.command == 'POST':
|
|
39
|
+
content_length = int(self.headers.get('Content-Length') or 0)
|
|
40
|
+
body = self.rfile.read(content_length) if content_length else b''
|
|
41
|
+
if body:
|
|
42
|
+
try:
|
|
43
|
+
payload = json.loads(body.decode('utf-8'))
|
|
44
|
+
token = payload.get('token')
|
|
45
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
46
|
+
self._send_json(400, {'error': 'invalid_payload'})
|
|
47
|
+
return
|
|
48
|
+
else:
|
|
49
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
50
|
+
params = urllib.parse.parse_qs(parsed_path.query)
|
|
51
|
+
token_values = params.get('token')
|
|
52
|
+
if token_values:
|
|
53
|
+
token = token_values[0]
|
|
54
|
+
|
|
55
|
+
if expected_token and token != expected_token:
|
|
56
|
+
self._send_json(403, {'error': 'invalid_token'})
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self._send_json(200, {'status': 'stopping'})
|
|
60
|
+
|
|
61
|
+
def shutdown_server(server):
|
|
62
|
+
time.sleep(0.05) # allow response to flush
|
|
63
|
+
server.shutdown()
|
|
64
|
+
|
|
65
|
+
threading.Thread(target=shutdown_server, args=(self.server,), daemon=True).start()
|