htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,486 @@
1
+ from __future__ import annotations
2
+
3
+ """HtmlGraph CLI - Track management commands."""
4
+
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Literal, cast
9
+
10
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
11
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
12
+
13
+ if TYPE_CHECKING:
14
+ from argparse import _SubParsersAction
15
+
16
+
17
+ def register_track_commands(subparsers: _SubParsersAction) -> None:
18
+ """Register track management commands."""
19
+ track_parser = subparsers.add_parser("track", help="Track management")
20
+ track_subparsers = track_parser.add_subparsers(
21
+ dest="track_command", help="Track command"
22
+ )
23
+
24
+ # track new
25
+ track_new = track_subparsers.add_parser("new", help="Create a new track")
26
+ track_new.add_argument("title", help="Track title")
27
+ track_new.add_argument("--description", help="Track description")
28
+ track_new.add_argument(
29
+ "--priority", choices=["low", "medium", "high"], default="medium"
30
+ )
31
+ track_new.add_argument(
32
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
33
+ )
34
+ track_new.add_argument(
35
+ "--format", choices=["json", "text"], default="text", help="Output format"
36
+ )
37
+ track_new.set_defaults(func=TrackNewCommand.from_args)
38
+
39
+ # track list
40
+ track_list = track_subparsers.add_parser("list", help="List all tracks")
41
+ track_list.add_argument(
42
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
43
+ )
44
+ track_list.add_argument(
45
+ "--format", choices=["json", "text"], default="text", help="Output format"
46
+ )
47
+ track_list.set_defaults(func=TrackListCommand.from_args)
48
+
49
+ # track spec
50
+ track_spec = track_subparsers.add_parser("spec", help="Create track spec")
51
+ track_spec.add_argument("track_id", help="Track ID")
52
+ track_spec.add_argument("title", help="Spec title")
53
+ track_spec.add_argument("--overview", help="Spec overview")
54
+ track_spec.add_argument("--context", help="Spec context")
55
+ track_spec.add_argument("--author", help="Spec author")
56
+ track_spec.add_argument(
57
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
58
+ )
59
+ track_spec.add_argument(
60
+ "--format", choices=["json", "text"], default="text", help="Output format"
61
+ )
62
+ track_spec.set_defaults(func=TrackSpecCommand.from_args)
63
+
64
+ # track plan
65
+ track_plan = track_subparsers.add_parser("plan", help="Create track plan")
66
+ track_plan.add_argument("track_id", help="Track ID")
67
+ track_plan.add_argument("title", help="Plan title")
68
+ track_plan.add_argument(
69
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
70
+ )
71
+ track_plan.add_argument(
72
+ "--format", choices=["json", "text"], default="text", help="Output format"
73
+ )
74
+ track_plan.set_defaults(func=TrackPlanCommand.from_args)
75
+
76
+ # track delete
77
+ track_delete = track_subparsers.add_parser("delete", help="Delete a track")
78
+ track_delete.add_argument("track_id", help="Track ID")
79
+ track_delete.add_argument(
80
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
81
+ )
82
+ track_delete.add_argument(
83
+ "--format", choices=["json", "text"], default="text", help="Output format"
84
+ )
85
+ track_delete.set_defaults(func=TrackDeleteCommand.from_args)
86
+
87
+
88
+ # ============================================================================
89
+ # Track Commands
90
+ # ============================================================================
91
+
92
+
93
+ class TrackNewCommand(BaseCommand):
94
+ """Create a new track."""
95
+
96
+ def __init__(
97
+ self,
98
+ *,
99
+ title: str,
100
+ description: str | None,
101
+ priority: str,
102
+ ) -> None:
103
+ super().__init__()
104
+ self.title = title
105
+ self.description = description
106
+ self.priority = priority
107
+
108
+ @classmethod
109
+ def from_args(cls, args: argparse.Namespace) -> TrackNewCommand:
110
+ return cls(
111
+ title=args.title, description=args.description, priority=args.priority
112
+ )
113
+
114
+ def execute(self) -> CommandResult:
115
+ """Create a new track."""
116
+ from htmlgraph.track_manager import TrackManager
117
+
118
+ if self.graph_dir is None:
119
+ raise CommandError("Missing graph directory")
120
+
121
+ manager = TrackManager(self.graph_dir)
122
+
123
+ # Type cast priority to expected literal type
124
+ priority_typed = cast(
125
+ Literal["low", "medium", "high", "critical"],
126
+ self.priority,
127
+ )
128
+
129
+ try:
130
+ track = manager.create_track(
131
+ title=self.title,
132
+ description=self.description or "",
133
+ priority=priority_typed,
134
+ )
135
+ except ValueError as e:
136
+ raise CommandError(str(e))
137
+
138
+ from htmlgraph.cli.base import TextOutputBuilder
139
+
140
+ output = TextOutputBuilder()
141
+ output.add_success(f"Created track: {track.id}")
142
+ output.add_field("Title", track.title)
143
+ output.add_field("Status", track.status)
144
+ output.add_field("Priority", track.priority)
145
+ output.add_field("Path", f"{self.graph_dir}/tracks/{track.id}/")
146
+ output.add_blank()
147
+ output.add_line("Next steps:")
148
+ output.add_field(
149
+ "- Create spec", f"htmlgraph track spec {track.id} 'Spec Title'"
150
+ )
151
+ output.add_field(
152
+ "- Create plan", f"htmlgraph track plan {track.id} 'Plan Title'"
153
+ )
154
+
155
+ json_data = {
156
+ "id": track.id,
157
+ "title": track.title,
158
+ "status": track.status,
159
+ "priority": track.priority,
160
+ "path": f"{self.graph_dir}/tracks/{track.id}/",
161
+ }
162
+
163
+ return CommandResult(
164
+ data=track,
165
+ text=output.build(),
166
+ json_data=json_data,
167
+ )
168
+
169
+
170
+ class TrackListCommand(BaseCommand):
171
+ """List all tracks."""
172
+
173
+ def __init__(
174
+ self,
175
+ *,
176
+ status: str | None = None,
177
+ priority: str | None = None,
178
+ has_spec: bool | None = None,
179
+ has_plan: bool | None = None,
180
+ ) -> None:
181
+ super().__init__()
182
+ self.status = status
183
+ self.priority = priority
184
+ self.has_spec = has_spec
185
+ self.has_plan = has_plan
186
+
187
+ @classmethod
188
+ def from_args(cls, args: argparse.Namespace) -> TrackListCommand:
189
+ # Validate inputs using TrackFilter model
190
+ from htmlgraph.cli.models import TrackFilter
191
+
192
+ # Get optional filter arguments
193
+ status = getattr(args, "status", None)
194
+ priority = getattr(args, "priority", None)
195
+ has_spec = getattr(args, "has_spec", None)
196
+ has_plan = getattr(args, "has_plan", None)
197
+
198
+ try:
199
+ filter_model = TrackFilter(
200
+ status=status, priority=priority, has_spec=has_spec, has_plan=has_plan
201
+ )
202
+ except ValueError as e:
203
+ raise CommandError(str(e))
204
+
205
+ return cls(
206
+ status=filter_model.status,
207
+ priority=filter_model.priority,
208
+ has_spec=filter_model.has_spec,
209
+ has_plan=filter_model.has_plan,
210
+ )
211
+
212
+ def execute(self) -> CommandResult:
213
+ """List all tracks."""
214
+ from htmlgraph.track_manager import TrackManager
215
+
216
+ if self.graph_dir is None:
217
+ raise CommandError("Missing graph directory")
218
+
219
+ manager = TrackManager(self.graph_dir)
220
+ track_ids = manager.list_tracks()
221
+
222
+ if not track_ids:
223
+ from htmlgraph.cli.base import TextOutputBuilder
224
+
225
+ output = TextOutputBuilder()
226
+ output.add_warning("No tracks found.")
227
+ output.add_blank()
228
+ output.add_dim("Create a track with: htmlgraph track new 'Track Title'")
229
+
230
+ return CommandResult(
231
+ text=output.build(),
232
+ json_data={"tracks": []},
233
+ )
234
+
235
+ # Create Rich table
236
+ from htmlgraph.cli.base import TableBuilder
237
+
238
+ builder = TableBuilder.create_list_table(f"Tracks in {self.graph_dir}/tracks/")
239
+ builder.add_id_column("Track ID", no_wrap=True)
240
+ builder.add_column("Components", style="green")
241
+ builder.add_column("Format", style="blue")
242
+
243
+ # Convert to display models for type-safe filtering
244
+ from htmlgraph.cli.models import TrackDisplay
245
+
246
+ display_tracks = []
247
+
248
+ for track_id in track_ids:
249
+ # Check for both consolidated and directory-based formats
250
+ track_file = Path(self.graph_dir) / "tracks" / f"{track_id}.html"
251
+ track_dir = Path(self.graph_dir) / "tracks" / track_id
252
+
253
+ if track_file.exists():
254
+ # Consolidated format
255
+ content = track_file.read_text(encoding="utf-8")
256
+ has_spec = (
257
+ 'data-section="overview"' in content
258
+ or 'data-section="requirements"' in content
259
+ )
260
+ has_plan = 'data-section="plan"' in content
261
+ format_type = "consolidated"
262
+ else:
263
+ # Directory format
264
+ has_spec = (track_dir / "spec.html").exists()
265
+ has_plan = (track_dir / "plan.html").exists()
266
+ format_type = "directory"
267
+
268
+ # Create display model
269
+ track_display = TrackDisplay.from_track_id(
270
+ track_id=track_id,
271
+ has_spec=has_spec,
272
+ has_plan=has_plan,
273
+ format_type=format_type,
274
+ )
275
+
276
+ # Apply filters
277
+ if self.has_spec is not None and track_display.has_spec != self.has_spec:
278
+ continue
279
+ if self.has_plan is not None and track_display.has_plan != self.has_plan:
280
+ continue
281
+
282
+ display_tracks.append(track_display)
283
+
284
+ for track in display_tracks:
285
+ builder.add_row(track.id, track.components_str, track.format_type)
286
+
287
+ # Return table object directly - TextFormatter will print it properly
288
+ return CommandResult(
289
+ data=builder.table,
290
+ json_data={"tracks": track_ids},
291
+ )
292
+
293
+
294
+ class TrackSpecCommand(BaseCommand):
295
+ """Create track spec."""
296
+
297
+ def __init__(
298
+ self,
299
+ *,
300
+ track_id: str,
301
+ title: str,
302
+ overview: str | None,
303
+ context: str | None,
304
+ author: str | None,
305
+ ) -> None:
306
+ super().__init__()
307
+ self.track_id = track_id
308
+ self.title = title
309
+ self.overview = overview
310
+ self.context = context
311
+ self.author = author
312
+
313
+ @classmethod
314
+ def from_args(cls, args: argparse.Namespace) -> TrackSpecCommand:
315
+ return cls(
316
+ track_id=args.track_id,
317
+ title=args.title,
318
+ overview=args.overview,
319
+ context=args.context,
320
+ author=args.author,
321
+ )
322
+
323
+ def execute(self) -> CommandResult:
324
+ """Create track spec."""
325
+ from htmlgraph.track_manager import TrackManager
326
+
327
+ if self.graph_dir is None:
328
+ raise CommandError("Missing graph directory")
329
+
330
+ manager = TrackManager(self.graph_dir)
331
+
332
+ # Check if track uses consolidated format
333
+ if manager.is_consolidated(self.track_id):
334
+ track_file = manager.tracks_dir / f"{self.track_id}.html"
335
+ msg = [
336
+ f"Track '{self.track_id}' uses consolidated single-file format.",
337
+ f"Spec is embedded in: {track_file}",
338
+ "\nTo create a track with separate spec/plan files, use:",
339
+ ' sdk.tracks.builder().separate_files().title("...").create()',
340
+ ]
341
+ return CommandResult(text="\n".join(msg))
342
+
343
+ try:
344
+ spec = manager.create_spec(
345
+ track_id=self.track_id,
346
+ title=self.title,
347
+ overview=self.overview or "",
348
+ context=self.context or "",
349
+ author=self.author or "",
350
+ )
351
+ except (ValueError, FileNotFoundError) as e:
352
+ raise CommandError(str(e))
353
+
354
+ from htmlgraph.cli.base import TextOutputBuilder
355
+
356
+ output = TextOutputBuilder()
357
+ output.add_success(f"Created spec: {spec.id}")
358
+ output.add_field("Title", spec.title)
359
+ output.add_field("Track", spec.track_id)
360
+ output.add_field("Status", spec.status)
361
+ output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/spec.html")
362
+ output.add_blank()
363
+ output.add_line(
364
+ f"View spec: open {self.graph_dir}/tracks/{self.track_id}/spec.html"
365
+ )
366
+
367
+ json_data = {
368
+ "id": spec.id,
369
+ "title": spec.title,
370
+ "track_id": spec.track_id,
371
+ "status": spec.status,
372
+ "path": f"{self.graph_dir}/tracks/{self.track_id}/spec.html",
373
+ }
374
+
375
+ return CommandResult(
376
+ data=spec,
377
+ text=output.build(),
378
+ json_data=json_data,
379
+ )
380
+
381
+
382
+ class TrackPlanCommand(BaseCommand):
383
+ """Create track plan."""
384
+
385
+ def __init__(self, *, track_id: str, title: str) -> None:
386
+ super().__init__()
387
+ self.track_id = track_id
388
+ self.title = title
389
+
390
+ @classmethod
391
+ def from_args(cls, args: argparse.Namespace) -> TrackPlanCommand:
392
+ return cls(track_id=args.track_id, title=args.title)
393
+
394
+ def execute(self) -> CommandResult:
395
+ """Create track plan."""
396
+ from htmlgraph.track_manager import TrackManager
397
+
398
+ if self.graph_dir is None:
399
+ raise CommandError("Missing graph directory")
400
+
401
+ manager = TrackManager(self.graph_dir)
402
+
403
+ # Check if track uses consolidated format
404
+ if manager.is_consolidated(self.track_id):
405
+ track_file = manager.tracks_dir / f"{self.track_id}.html"
406
+ msg = [
407
+ f"Track '{self.track_id}' uses consolidated single-file format.",
408
+ f"Plan is embedded in: {track_file}",
409
+ "\nTo create a track with separate spec/plan files, use:",
410
+ ' sdk.tracks.builder().separate_files().title("...").create()',
411
+ ]
412
+ return CommandResult(text="\n".join(msg))
413
+
414
+ try:
415
+ plan = manager.create_plan(
416
+ track_id=self.track_id,
417
+ title=self.title,
418
+ )
419
+ except (ValueError, FileNotFoundError) as e:
420
+ raise CommandError(str(e))
421
+
422
+ from htmlgraph.cli.base import TextOutputBuilder
423
+
424
+ output = TextOutputBuilder()
425
+ output.add_success(f"Created plan: {plan.id}")
426
+ output.add_field("Title", plan.title)
427
+ output.add_field("Track", plan.track_id)
428
+ output.add_field("Status", plan.status)
429
+ output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/plan.html")
430
+ output.add_blank()
431
+ output.add_line(
432
+ f"View plan: open {self.graph_dir}/tracks/{self.track_id}/plan.html"
433
+ )
434
+
435
+ json_data = {
436
+ "id": plan.id,
437
+ "title": plan.title,
438
+ "track_id": plan.track_id,
439
+ "status": plan.status,
440
+ "path": f"{self.graph_dir}/tracks/{self.track_id}/plan.html",
441
+ }
442
+
443
+ return CommandResult(
444
+ data=plan,
445
+ text=output.build(),
446
+ json_data=json_data,
447
+ )
448
+
449
+
450
+ class TrackDeleteCommand(BaseCommand):
451
+ """Delete a track."""
452
+
453
+ def __init__(self, *, track_id: str) -> None:
454
+ super().__init__()
455
+ self.track_id = track_id
456
+
457
+ @classmethod
458
+ def from_args(cls, args: argparse.Namespace) -> TrackDeleteCommand:
459
+ return cls(track_id=args.track_id)
460
+
461
+ def execute(self) -> CommandResult:
462
+ """Delete a track."""
463
+ from htmlgraph.track_manager import TrackManager
464
+
465
+ if self.graph_dir is None:
466
+ raise CommandError("Missing graph directory")
467
+
468
+ manager = TrackManager(self.graph_dir)
469
+
470
+ try:
471
+ manager.delete_track(self.track_id)
472
+ except ValueError as e:
473
+ raise CommandError(str(e))
474
+
475
+ from htmlgraph.cli.base import TextOutputBuilder
476
+
477
+ output = TextOutputBuilder()
478
+ output.add_success(f"Deleted track: {self.track_id}")
479
+ output.add_field("Removed", f"{self.graph_dir}/tracks/{self.track_id}/")
480
+
481
+ json_data = {"deleted": True, "track_id": self.track_id}
482
+
483
+ return CommandResult(
484
+ text=output.build(),
485
+ json_data=json_data,
486
+ )
@@ -0,0 +1 @@
1
+ """CLI command implementations."""
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.prompt import Prompt
8
+ from rich.table import Table
9
+
10
+ from htmlgraph.cli_framework import BaseCommand, CommandError, CommandResult
11
+
12
+ _console = Console()
13
+
14
+
15
+ class FeatureCreateCommand(BaseCommand):
16
+ def __init__(
17
+ self,
18
+ *,
19
+ collection: str,
20
+ title: str,
21
+ description: str,
22
+ priority: str,
23
+ steps: Iterable[str] | None,
24
+ track_id: str | None = None,
25
+ ) -> None:
26
+ super().__init__()
27
+ self.collection = collection
28
+ self.title = title
29
+ self.description = description
30
+ self.priority = priority
31
+ self.steps = list(steps) if steps else []
32
+ self.track_id = track_id
33
+
34
+ def execute(self) -> CommandResult:
35
+ sdk = self.get_sdk()
36
+
37
+ # Determine track_id for feature creation
38
+ track_id = self.track_id
39
+
40
+ # Only enforce track selection for main features collection
41
+ if self.collection == "features":
42
+ if not track_id:
43
+ # Get available tracks
44
+ try:
45
+ tracks = sdk.tracks.all()
46
+ if not tracks:
47
+ raise CommandError(
48
+ "No tracks found. Create a track first:\n"
49
+ " uv run htmlgraph track new 'Track Title'"
50
+ )
51
+
52
+ if len(tracks) == 1:
53
+ # Auto-select if only one track exists
54
+ track_id = tracks[0].id
55
+ _console.print(
56
+ f"[dim]Auto-selected track: {tracks[0].title}[/dim]"
57
+ )
58
+ else:
59
+ # Interactive selection
60
+ _console.print("[bold]Available Tracks:[/bold]")
61
+ for i, track in enumerate(tracks, 1):
62
+ _console.print(f" {i}. {track.title} ({track.id})")
63
+
64
+ selection = Prompt.ask(
65
+ "Select track",
66
+ choices=[str(i) for i in range(1, len(tracks) + 1)],
67
+ )
68
+ track_id = tracks[int(selection) - 1].id
69
+ except Exception as e:
70
+ raise CommandError(f"Failed to get available tracks: {e}")
71
+
72
+ builder = sdk.features.create(
73
+ title=self.title,
74
+ description=self.description,
75
+ priority=self.priority,
76
+ )
77
+ if self.steps:
78
+ builder.add_steps(self.steps)
79
+ if track_id:
80
+ builder.set_track(track_id)
81
+ node = builder.save()
82
+ else:
83
+ node = sdk.session_manager.create_feature(
84
+ title=self.title,
85
+ collection=self.collection,
86
+ description=self.description,
87
+ priority=self.priority,
88
+ steps=self.steps,
89
+ agent=self.agent,
90
+ )
91
+
92
+ # Format output with Rich
93
+ table = Table(show_header=False, box=None)
94
+ table.add_column(style="bold cyan")
95
+ table.add_column()
96
+
97
+ table.add_row("Created:", f"[green]{node.id}[/green]")
98
+ table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
99
+ table.add_row("Status:", f"[blue]{node.status}[/blue]")
100
+ if node.track_id:
101
+ table.add_row("Track:", f"[cyan]{node.track_id}[/cyan]")
102
+ table.add_row(
103
+ "Path:", f"[dim]{self.graph_dir}/{self.collection}/{node.id}.html[/dim]"
104
+ )
105
+
106
+ # Format as Rich panel for text output
107
+ text = [
108
+ f"Created: {node.id}",
109
+ f" Title: {node.title}",
110
+ f" Status: {node.status}",
111
+ ]
112
+ if node.track_id:
113
+ text.append(f" Track: {node.track_id}")
114
+ text.append(f" Path: {self.graph_dir}/{self.collection}/{node.id}.html")
115
+
116
+ return CommandResult(data=node, text=text)
117
+
118
+
119
+ class FeatureStartCommand(BaseCommand):
120
+ def __init__(self, *, collection: str, feature_id: str) -> None:
121
+ super().__init__()
122
+ self.collection = collection
123
+ self.feature_id = feature_id
124
+
125
+ def execute(self) -> CommandResult:
126
+ sdk = self.get_sdk()
127
+ collection = getattr(sdk, self.collection, None)
128
+
129
+ if not collection:
130
+ raise CommandError(f"Collection '{self.collection}' not found in SDK.")
131
+
132
+ node = collection.start(self.feature_id)
133
+ if node is None:
134
+ raise CommandError(
135
+ f"Feature '{self.feature_id}' not found in {self.collection}."
136
+ )
137
+
138
+ status = sdk.session_manager.get_status()
139
+
140
+ # Format output with Rich
141
+ table = Table(show_header=False, box=None)
142
+ table.add_column(style="bold cyan")
143
+ table.add_column()
144
+
145
+ table.add_row("Started:", f"[green]{node.id}[/green]")
146
+ table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
147
+ table.add_row("Status:", f"[blue]{node.status}[/blue]")
148
+ wip_color = "red" if status["wip_count"] >= status["wip_limit"] else "green"
149
+ table.add_row(
150
+ "WIP:",
151
+ f"[{wip_color}]{status['wip_count']}/{status['wip_limit']}[/{wip_color}]",
152
+ )
153
+
154
+ text = [
155
+ f"Started: {node.id}",
156
+ f" Title: {node.title}",
157
+ f" Status: {node.status}",
158
+ f" WIP: {status['wip_count']}/{status['wip_limit']}",
159
+ ]
160
+ return CommandResult(data=node, text=text)
161
+
162
+
163
+ class FeatureCompleteCommand(BaseCommand):
164
+ def __init__(self, *, collection: str, feature_id: str) -> None:
165
+ super().__init__()
166
+ self.collection = collection
167
+ self.feature_id = feature_id
168
+
169
+ def execute(self) -> CommandResult:
170
+ sdk = self.get_sdk()
171
+ collection = getattr(sdk, self.collection, None)
172
+
173
+ if not collection:
174
+ raise CommandError(f"Collection '{self.collection}' not found in SDK.")
175
+
176
+ node = collection.complete(self.feature_id)
177
+ if node is None:
178
+ raise CommandError(
179
+ f"Feature '{self.feature_id}' not found in {self.collection}."
180
+ )
181
+
182
+ # Format output with Rich
183
+ panel = Panel(
184
+ f"[bold green]✓ Completed[/bold green]\n"
185
+ f"[cyan]{node.id}[/cyan]\n"
186
+ f"[yellow]{node.title}[/yellow]",
187
+ border_style="green",
188
+ )
189
+ _console.print(panel)
190
+
191
+ text = [
192
+ f"Completed: {node.id}",
193
+ f" Title: {node.title}",
194
+ ]
195
+ return CommandResult(data=node, text=text)