htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__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.
- htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +50 -10
- htmlgraph/api/templates/dashboard-redesign.html +608 -54
- htmlgraph/api/templates/partials/activity-feed.html +21 -0
- htmlgraph/api/templates/partials/features.html +81 -12
- htmlgraph/api/templates/partials/orchestration.html +35 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +939 -0
- htmlgraph/cli/base.py +660 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +856 -0
- htmlgraph/cli/main.py +143 -0
- htmlgraph/cli/models.py +462 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +398 -0
- htmlgraph/cli/work/__init__.py +159 -0
- htmlgraph/cli/work/features.py +567 -0
- htmlgraph/cli/work/orchestration.py +675 -0
- htmlgraph/cli/work/sessions.py +465 -0
- htmlgraph/cli/work/tracks.py +485 -0
- htmlgraph/dashboard.html +6414 -634
- htmlgraph/db/schema.py +8 -3
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
- htmlgraph/docs/README.md +2 -3
- htmlgraph/hooks/event_tracker.py +355 -26
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/orchestrator.py +137 -71
- htmlgraph/hooks/orchestrator_reflector.py +23 -0
- htmlgraph/hooks/pretooluse.py +29 -6
- htmlgraph/hooks/session_handler.py +28 -0
- htmlgraph/hooks/session_summary.py +391 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +71 -12
- htmlgraph/hooks/validator.py +192 -79
- htmlgraph/operations/__init__.py +18 -0
- htmlgraph/operations/initialization.py +596 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/orchestration/__init__.py +16 -1
- htmlgraph/orchestration/claude_launcher.py +185 -0
- htmlgraph/orchestration/command_builder.py +71 -0
- htmlgraph/orchestration/headless_spawner.py +72 -1332
- htmlgraph/orchestration/plugin_manager.py +136 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +170 -0
- htmlgraph/orchestration/spawners/codex.py +442 -0
- htmlgraph/orchestration/spawners/copilot.py +299 -0
- htmlgraph/orchestration/spawners/gemini.py +478 -0
- htmlgraph/orchestration/subprocess_runner.py +33 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +45 -12
- htmlgraph/transcript.py +16 -4
- htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -7256
- htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
htmlgraph/cli/main.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""HtmlGraph CLI - Main entry point.
|
|
2
|
+
|
|
3
|
+
Entry point with argument parsing and command routing.
|
|
4
|
+
Keeps main() thin by delegating to command modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from htmlgraph.cli.constants import (
|
|
15
|
+
DEFAULT_GRAPH_DIR,
|
|
16
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
17
|
+
OUTPUT_FORMATS,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
22
|
+
"""Create and configure the argument parser.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Configured ArgumentParser with all subcommands
|
|
26
|
+
"""
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
description="HtmlGraph - HTML is All You Need",
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
epilog="""
|
|
31
|
+
Examples:
|
|
32
|
+
htmlgraph init # Initialize .htmlgraph in current dir
|
|
33
|
+
htmlgraph serve # Start server on port 8080
|
|
34
|
+
htmlgraph status # Show graph status
|
|
35
|
+
htmlgraph query "[data-status='todo']" # Query nodes
|
|
36
|
+
|
|
37
|
+
Session Management:
|
|
38
|
+
htmlgraph session start # Start a new session (auto-ID)
|
|
39
|
+
htmlgraph session end my-session # End a session
|
|
40
|
+
htmlgraph session list # List all sessions
|
|
41
|
+
|
|
42
|
+
Feature Management:
|
|
43
|
+
htmlgraph feature list # List all features
|
|
44
|
+
htmlgraph feature start feat-001 # Start working on a feature
|
|
45
|
+
htmlgraph feature complete feat-001 # Mark feature as done
|
|
46
|
+
|
|
47
|
+
Track Management:
|
|
48
|
+
htmlgraph track new "User Auth" # Create a new track
|
|
49
|
+
htmlgraph track list # List all tracks
|
|
50
|
+
|
|
51
|
+
Analytics:
|
|
52
|
+
htmlgraph analytics # Project-wide analytics
|
|
53
|
+
htmlgraph analytics --recent 10 # Analyze last 10 sessions
|
|
54
|
+
|
|
55
|
+
For more help: https://github.com/Shakes-tzd/htmlgraph
|
|
56
|
+
""",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Global output control flags
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--format",
|
|
62
|
+
choices=OUTPUT_FORMATS,
|
|
63
|
+
default=DEFAULT_OUTPUT_FORMAT,
|
|
64
|
+
help="Output format: text (default), json, or plain",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--quiet",
|
|
68
|
+
"-q",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="Suppress progress messages and non-essential output",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--verbose",
|
|
74
|
+
"-v",
|
|
75
|
+
action="count",
|
|
76
|
+
default=0,
|
|
77
|
+
help="Increase verbosity (can be used multiple times: -v, -vv, -vvv)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
81
|
+
|
|
82
|
+
# Import command registration functions
|
|
83
|
+
from htmlgraph.cli import analytics, core, work
|
|
84
|
+
|
|
85
|
+
# Register commands from each module
|
|
86
|
+
core.register_commands(subparsers)
|
|
87
|
+
work.register_commands(subparsers)
|
|
88
|
+
analytics.register_commands(subparsers)
|
|
89
|
+
|
|
90
|
+
return parser
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main() -> None:
|
|
94
|
+
"""Main entry point for the CLI."""
|
|
95
|
+
parser = create_parser()
|
|
96
|
+
args = parser.parse_args()
|
|
97
|
+
|
|
98
|
+
# If no command specified, show help
|
|
99
|
+
if not args.command:
|
|
100
|
+
parser.print_help()
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
# Get the command handler (set by register_commands via set_defaults)
|
|
104
|
+
if not hasattr(args, "func"):
|
|
105
|
+
parser.print_help()
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
# Determine graph directory and agent
|
|
109
|
+
graph_dir = getattr(args, "graph_dir", DEFAULT_GRAPH_DIR)
|
|
110
|
+
agent = getattr(args, "agent", None)
|
|
111
|
+
|
|
112
|
+
# Get output format from args
|
|
113
|
+
output_format = args.format
|
|
114
|
+
|
|
115
|
+
# Execute the command
|
|
116
|
+
try:
|
|
117
|
+
# Create command instance from args
|
|
118
|
+
command = args.func(args)
|
|
119
|
+
|
|
120
|
+
# Run command with context
|
|
121
|
+
command.run(
|
|
122
|
+
graph_dir=graph_dir,
|
|
123
|
+
agent=agent,
|
|
124
|
+
output_format=output_format,
|
|
125
|
+
)
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
err_console = Console(stderr=True)
|
|
128
|
+
err_console.print("\n\n[yellow]Interrupted by user[/yellow]")
|
|
129
|
+
sys.exit(130)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
from htmlgraph.cli.base import save_traceback
|
|
132
|
+
|
|
133
|
+
err_console = Console(stderr=True)
|
|
134
|
+
log_file = save_traceback(
|
|
135
|
+
e, context={"command": args.command, "cwd": graph_dir}
|
|
136
|
+
)
|
|
137
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
138
|
+
err_console.print(f"[dim]Full traceback saved to:[/dim] {log_file}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
main()
|
htmlgraph/cli/models.py
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""HtmlGraph CLI - Pydantic models for command filters and configuration.
|
|
2
|
+
|
|
3
|
+
This module provides type-safe models for validating command inputs:
|
|
4
|
+
- Filter models for list commands (features, sessions, tracks)
|
|
5
|
+
- Configuration models for infrastructure commands (init, serve)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field, field_validator
|
|
14
|
+
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# Filter Models
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FeatureFilter(BaseModel):
|
|
21
|
+
"""Filter options for feature listing.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
status: Filter by status (todo, in_progress, completed, blocked, all)
|
|
25
|
+
priority: Filter by priority (high, medium, low, critical, all)
|
|
26
|
+
agent: Filter by agent name
|
|
27
|
+
collection: Collection name to query (default: features)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
status: Literal["todo", "in_progress", "completed", "blocked", "all"] | None = None
|
|
31
|
+
priority: Literal["high", "medium", "low", "critical", "all"] | None = None
|
|
32
|
+
agent: str | None = None
|
|
33
|
+
collection: str = Field(default="features")
|
|
34
|
+
|
|
35
|
+
@field_validator("status")
|
|
36
|
+
@classmethod
|
|
37
|
+
def validate_status(cls, v: str | None) -> str | None:
|
|
38
|
+
"""Validate status value."""
|
|
39
|
+
if v and v not in ["todo", "in_progress", "completed", "blocked", "all"]:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Invalid status: {v}. "
|
|
42
|
+
f"Valid values: todo, in_progress, completed, blocked, all"
|
|
43
|
+
)
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
@field_validator("priority")
|
|
47
|
+
@classmethod
|
|
48
|
+
def validate_priority(cls, v: str | None) -> str | None:
|
|
49
|
+
"""Validate priority value."""
|
|
50
|
+
if v and v not in ["high", "medium", "low", "critical", "all"]:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Invalid priority: {v}. Valid values: high, medium, low, critical, all"
|
|
53
|
+
)
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SessionFilter(BaseModel):
|
|
58
|
+
"""Filter options for session listing.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
status: Filter by status (active, ended, all)
|
|
62
|
+
agent: Filter by agent name
|
|
63
|
+
since: Only show sessions since this date
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
status: Literal["active", "ended", "all"] | None = None
|
|
67
|
+
agent: str | None = None
|
|
68
|
+
since: datetime | None = None
|
|
69
|
+
|
|
70
|
+
@field_validator("status")
|
|
71
|
+
@classmethod
|
|
72
|
+
def validate_status(cls, v: str | None) -> str | None:
|
|
73
|
+
"""Validate status value."""
|
|
74
|
+
if v and v not in ["active", "ended", "all"]:
|
|
75
|
+
raise ValueError(f"Invalid status: {v}. Valid values: active, ended, all")
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TrackFilter(BaseModel):
|
|
80
|
+
"""Filter options for track listing.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
status: Filter by status (todo, in_progress, completed, all)
|
|
84
|
+
priority: Filter by priority (high, medium, low, all)
|
|
85
|
+
has_spec: Filter for tracks with specs
|
|
86
|
+
has_plan: Filter for tracks with plans
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
status: Literal["todo", "in_progress", "completed", "all"] | None = None
|
|
90
|
+
priority: Literal["high", "medium", "low", "all"] | None = None
|
|
91
|
+
has_spec: bool | None = None
|
|
92
|
+
has_plan: bool | None = None
|
|
93
|
+
|
|
94
|
+
@field_validator("status")
|
|
95
|
+
@classmethod
|
|
96
|
+
def validate_status(cls, v: str | None) -> str | None:
|
|
97
|
+
"""Validate status value."""
|
|
98
|
+
if v and v not in ["todo", "in_progress", "completed", "all"]:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"Invalid status: {v}. Valid values: todo, in_progress, completed, all"
|
|
101
|
+
)
|
|
102
|
+
return v
|
|
103
|
+
|
|
104
|
+
@field_validator("priority")
|
|
105
|
+
@classmethod
|
|
106
|
+
def validate_priority(cls, v: str | None) -> str | None:
|
|
107
|
+
"""Validate priority value."""
|
|
108
|
+
if v and v not in ["high", "medium", "low", "all"]:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Invalid priority: {v}. Valid values: high, medium, low, all"
|
|
111
|
+
)
|
|
112
|
+
return v
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# Configuration Models
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class InitConfig(BaseModel):
|
|
121
|
+
"""Configuration for htmlgraph init command.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
dir: Directory to initialize (default: .)
|
|
125
|
+
install_hooks: Install Git hooks for event logging
|
|
126
|
+
interactive: Interactive setup wizard
|
|
127
|
+
no_index: Do not create the analytics cache (index.sqlite)
|
|
128
|
+
no_update_gitignore: Do not update/create .gitignore for cache files
|
|
129
|
+
no_events_keep: Do not create .htmlgraph/events/.gitkeep
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
dir: str = Field(default=".")
|
|
133
|
+
install_hooks: bool = Field(default=False)
|
|
134
|
+
interactive: bool = Field(default=False)
|
|
135
|
+
no_index: bool = Field(default=False)
|
|
136
|
+
no_update_gitignore: bool = Field(default=False)
|
|
137
|
+
no_events_keep: bool = Field(default=False)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ServeConfig(BaseModel):
|
|
141
|
+
"""Configuration for htmlgraph serve command.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
port: Port to bind to (must be between 1024-65535)
|
|
145
|
+
host: Host to bind to (default: 0.0.0.0)
|
|
146
|
+
graph_dir: Graph directory path
|
|
147
|
+
static_dir: Static files directory
|
|
148
|
+
no_watch: Disable file watching (auto-reload disabled)
|
|
149
|
+
auto_port: Automatically find available port if default is occupied
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
port: int = Field(default=8080, ge=1024, le=65535)
|
|
153
|
+
host: str = Field(default="0.0.0.0")
|
|
154
|
+
graph_dir: str = Field(default=".htmlgraph")
|
|
155
|
+
static_dir: str = Field(default=".")
|
|
156
|
+
no_watch: bool = Field(default=False)
|
|
157
|
+
auto_port: bool = Field(default=False)
|
|
158
|
+
|
|
159
|
+
@field_validator("port")
|
|
160
|
+
@classmethod
|
|
161
|
+
def validate_port(cls, v: int) -> int:
|
|
162
|
+
"""Validate port is in valid range."""
|
|
163
|
+
if not 1024 <= v <= 65535:
|
|
164
|
+
raise ValueError(f"Port must be between 1024 and 65535, got {v}")
|
|
165
|
+
return v
|
|
166
|
+
|
|
167
|
+
@field_validator("host")
|
|
168
|
+
@classmethod
|
|
169
|
+
def validate_host(cls, v: str) -> str:
|
|
170
|
+
"""Validate host is not empty."""
|
|
171
|
+
if not v or not v.strip():
|
|
172
|
+
raise ValueError("Host cannot be empty")
|
|
173
|
+
return v.strip()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ServeApiConfig(BaseModel):
|
|
177
|
+
"""Configuration for htmlgraph serve-api command.
|
|
178
|
+
|
|
179
|
+
Attributes:
|
|
180
|
+
port: Port to bind to (must be between 1024-65535)
|
|
181
|
+
host: Host to bind to (default: 127.0.0.1)
|
|
182
|
+
db: Path to SQLite database file
|
|
183
|
+
auto_port: Automatically find available port if default is occupied
|
|
184
|
+
reload: Enable auto-reload on file changes (development mode)
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
port: int = Field(default=8000, ge=1024, le=65535)
|
|
188
|
+
host: str = Field(default="127.0.0.1")
|
|
189
|
+
db: str | None = None
|
|
190
|
+
auto_port: bool = Field(default=False)
|
|
191
|
+
reload: bool = Field(default=False)
|
|
192
|
+
|
|
193
|
+
@field_validator("port")
|
|
194
|
+
@classmethod
|
|
195
|
+
def validate_port(cls, v: int) -> int:
|
|
196
|
+
"""Validate port is in valid range."""
|
|
197
|
+
if not 1024 <= v <= 65535:
|
|
198
|
+
raise ValueError(f"Port must be between 1024 and 65535, got {v}")
|
|
199
|
+
return v
|
|
200
|
+
|
|
201
|
+
@field_validator("host")
|
|
202
|
+
@classmethod
|
|
203
|
+
def validate_host(cls, v: str) -> str:
|
|
204
|
+
"""Validate host is not empty."""
|
|
205
|
+
if not v or not v.strip():
|
|
206
|
+
raise ValueError("Host cannot be empty")
|
|
207
|
+
return v.strip()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ============================================================================
|
|
211
|
+
# Result Models
|
|
212
|
+
# ============================================================================
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class InitResult(BaseModel):
|
|
216
|
+
"""Result from htmlgraph init command.
|
|
217
|
+
|
|
218
|
+
Attributes:
|
|
219
|
+
success: Whether initialization succeeded
|
|
220
|
+
graph_dir: Path to initialized .htmlgraph directory
|
|
221
|
+
directories_created: List of directories created
|
|
222
|
+
files_created: List of files created
|
|
223
|
+
hooks_installed: Whether Git hooks were installed
|
|
224
|
+
warnings: List of warning messages
|
|
225
|
+
errors: List of error messages
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
success: bool = Field(default=True)
|
|
229
|
+
graph_dir: str = Field(...)
|
|
230
|
+
directories_created: list[str] = Field(default_factory=list)
|
|
231
|
+
files_created: list[str] = Field(default_factory=list)
|
|
232
|
+
hooks_installed: bool = Field(default=False)
|
|
233
|
+
warnings: list[str] = Field(default_factory=list)
|
|
234
|
+
errors: list[str] = Field(default_factory=list)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def summary(self) -> str:
|
|
238
|
+
"""Human-readable summary of initialization."""
|
|
239
|
+
lines = [f"Initialized {self.graph_dir}"]
|
|
240
|
+
if self.directories_created:
|
|
241
|
+
lines.append(f" • Created {len(self.directories_created)} directories")
|
|
242
|
+
if self.files_created:
|
|
243
|
+
lines.append(f" • Created {len(self.files_created)} files")
|
|
244
|
+
if self.hooks_installed:
|
|
245
|
+
lines.append(" • Installed Git hooks")
|
|
246
|
+
if self.warnings:
|
|
247
|
+
lines.append(f" • {len(self.warnings)} warnings")
|
|
248
|
+
if self.errors:
|
|
249
|
+
lines.append(f" • {len(self.errors)} errors")
|
|
250
|
+
return "\n".join(lines)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ValidationResult(BaseModel):
|
|
254
|
+
"""Result from directory validation.
|
|
255
|
+
|
|
256
|
+
Attributes:
|
|
257
|
+
valid: Whether directory is valid for initialization
|
|
258
|
+
exists: Whether directory already exists
|
|
259
|
+
is_initialized: Whether directory is already initialized
|
|
260
|
+
has_git: Whether directory is in a Git repository
|
|
261
|
+
errors: List of validation errors
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
valid: bool = Field(default=True)
|
|
265
|
+
exists: bool = Field(default=False)
|
|
266
|
+
is_initialized: bool = Field(default=False)
|
|
267
|
+
has_git: bool = Field(default=False)
|
|
268
|
+
errors: list[str] = Field(default_factory=list)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ============================================================================
|
|
272
|
+
# Display Models
|
|
273
|
+
# ============================================================================
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SessionDisplay(BaseModel):
|
|
277
|
+
"""Validated session data for CLI display."""
|
|
278
|
+
|
|
279
|
+
id: str = Field(..., description="Session identifier")
|
|
280
|
+
status: str = Field(default="active", description="Session status")
|
|
281
|
+
agent: str = Field(default="unknown", description="Agent name")
|
|
282
|
+
event_count: int = Field(default=0, ge=0, description="Number of events")
|
|
283
|
+
started_at: datetime = Field(..., description="Session start time")
|
|
284
|
+
ended_at: datetime | None = Field(None, description="Session end time")
|
|
285
|
+
title: str | None = Field(None, description="Session title")
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def from_node(cls, node: object) -> SessionDisplay:
|
|
289
|
+
"""
|
|
290
|
+
Create SessionDisplay from graph node.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
node: Session node from graph
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
SessionDisplay instance
|
|
297
|
+
"""
|
|
298
|
+
return cls(
|
|
299
|
+
id=getattr(node, "id"),
|
|
300
|
+
status=getattr(node, "status", "active"),
|
|
301
|
+
agent=getattr(node, "agent", "unknown"),
|
|
302
|
+
event_count=getattr(node, "event_count", 0),
|
|
303
|
+
started_at=getattr(node, "started_at"),
|
|
304
|
+
ended_at=getattr(node, "ended_at", None),
|
|
305
|
+
title=getattr(node, "title", None),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def started_str(self) -> str:
|
|
310
|
+
"""Formatted start time for display."""
|
|
311
|
+
return self.started_at.strftime("%Y-%m-%d %H:%M")
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def ended_str(self) -> str | None:
|
|
315
|
+
"""Formatted end time for display."""
|
|
316
|
+
if self.ended_at:
|
|
317
|
+
return self.ended_at.strftime("%Y-%m-%d %H:%M")
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def sort_key(self) -> datetime:
|
|
321
|
+
"""
|
|
322
|
+
Return sort key for session ordering.
|
|
323
|
+
|
|
324
|
+
Returns timezone-naive datetime for consistent sorting.
|
|
325
|
+
"""
|
|
326
|
+
ts = self.started_at
|
|
327
|
+
if ts.tzinfo is None:
|
|
328
|
+
return ts
|
|
329
|
+
return ts.replace(tzinfo=None)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class FeatureDisplay(BaseModel):
|
|
333
|
+
"""Validated feature data for CLI display."""
|
|
334
|
+
|
|
335
|
+
id: str = Field(..., description="Feature identifier")
|
|
336
|
+
title: str = Field(default="Untitled", description="Feature title")
|
|
337
|
+
status: str = Field(default="unknown", description="Feature status")
|
|
338
|
+
priority: Literal["low", "medium", "high", "critical"] = Field(
|
|
339
|
+
default="medium", description="Feature priority"
|
|
340
|
+
)
|
|
341
|
+
updated: datetime = Field(..., description="Last update time")
|
|
342
|
+
created: datetime | None = Field(None, description="Creation time")
|
|
343
|
+
track_id: str | None = Field(None, description="Associated track ID")
|
|
344
|
+
agent_assigned: str | None = Field(None, description="Assigned agent")
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def from_node(cls, node: object) -> FeatureDisplay:
|
|
348
|
+
"""
|
|
349
|
+
Create FeatureDisplay from graph node.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
node: Feature node from graph
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
FeatureDisplay instance
|
|
356
|
+
"""
|
|
357
|
+
return cls(
|
|
358
|
+
id=getattr(node, "id"),
|
|
359
|
+
title=getattr(node, "title", "Untitled"),
|
|
360
|
+
status=getattr(node, "status", "unknown"),
|
|
361
|
+
priority=getattr(node, "priority", "medium"),
|
|
362
|
+
updated=getattr(node, "updated"),
|
|
363
|
+
created=getattr(node, "created", None),
|
|
364
|
+
track_id=getattr(node, "track_id", None),
|
|
365
|
+
agent_assigned=getattr(node, "agent_assigned", None),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def updated_str(self) -> str:
|
|
370
|
+
"""Formatted update time for display."""
|
|
371
|
+
return self.updated.strftime("%Y-%m-%d %H:%M")
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def created_str(self) -> str | None:
|
|
375
|
+
"""Formatted creation time for display."""
|
|
376
|
+
if self.created:
|
|
377
|
+
return self.created.strftime("%Y-%m-%d %H:%M")
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
def sort_key(self) -> tuple[int, datetime]:
|
|
381
|
+
"""
|
|
382
|
+
Return sort key for feature ordering (priority, then updated time).
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Tuple of (priority_rank, timezone_naive_updated_time)
|
|
386
|
+
"""
|
|
387
|
+
priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
388
|
+
priority_rank = priority_order.get(self.priority, 99)
|
|
389
|
+
|
|
390
|
+
updated_ts = self.updated
|
|
391
|
+
if updated_ts.tzinfo is None:
|
|
392
|
+
updated_naive = updated_ts
|
|
393
|
+
else:
|
|
394
|
+
updated_naive = updated_ts.replace(tzinfo=None)
|
|
395
|
+
|
|
396
|
+
return (priority_rank, updated_naive)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TrackDisplay(BaseModel):
|
|
400
|
+
"""Validated track data for CLI display."""
|
|
401
|
+
|
|
402
|
+
id: str = Field(..., description="Track identifier")
|
|
403
|
+
title: str = Field(default="Untitled", description="Track title")
|
|
404
|
+
status: str = Field(default="planning", description="Track status")
|
|
405
|
+
priority: Literal["low", "medium", "high"] = Field(
|
|
406
|
+
default="medium", description="Track priority"
|
|
407
|
+
)
|
|
408
|
+
has_spec: bool = Field(default=False, description="Has specification")
|
|
409
|
+
has_plan: bool = Field(default=False, description="Has plan")
|
|
410
|
+
format_type: str = Field(
|
|
411
|
+
default="consolidated", description="File format (consolidated or directory)"
|
|
412
|
+
)
|
|
413
|
+
feature_count: int = Field(default=0, ge=0, description="Number of features")
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def from_track_id(
|
|
417
|
+
cls,
|
|
418
|
+
track_id: str,
|
|
419
|
+
has_spec: bool = False,
|
|
420
|
+
has_plan: bool = False,
|
|
421
|
+
format_type: str = "consolidated",
|
|
422
|
+
) -> TrackDisplay:
|
|
423
|
+
"""
|
|
424
|
+
Create TrackDisplay from track ID and metadata.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
track_id: Track identifier
|
|
428
|
+
has_spec: Whether track has specification
|
|
429
|
+
has_plan: Whether track has plan
|
|
430
|
+
format_type: File format type
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
TrackDisplay instance
|
|
434
|
+
"""
|
|
435
|
+
return cls(
|
|
436
|
+
id=track_id,
|
|
437
|
+
title=track_id, # Default to ID if title not available
|
|
438
|
+
has_spec=has_spec,
|
|
439
|
+
has_plan=has_plan,
|
|
440
|
+
format_type=format_type,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def components_str(self) -> str:
|
|
445
|
+
"""Formatted components list for display."""
|
|
446
|
+
components = []
|
|
447
|
+
if self.has_spec:
|
|
448
|
+
components.append("spec")
|
|
449
|
+
if self.has_plan:
|
|
450
|
+
components.append("plan")
|
|
451
|
+
return ", ".join(components) if components else "empty"
|
|
452
|
+
|
|
453
|
+
def sort_key(self) -> tuple[int, str]:
|
|
454
|
+
"""
|
|
455
|
+
Return sort key for track ordering (priority, then ID).
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Tuple of (priority_rank, track_id)
|
|
459
|
+
"""
|
|
460
|
+
priority_order = {"high": 0, "medium": 1, "low": 2}
|
|
461
|
+
priority_rank = priority_order.get(self.priority, 99)
|
|
462
|
+
return (priority_rank, self.id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTML templates for CLI output."""
|