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,115 @@
1
+ from __future__ import annotations
2
+
3
+ """HtmlGraph CLI - Browse command for opening dashboard in browser."""
4
+
5
+
6
+ import argparse
7
+ import webbrowser
8
+ from typing import TYPE_CHECKING
9
+
10
+ from htmlgraph.cli.base import BaseCommand, CommandResult
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ class BrowseCommand(BaseCommand):
17
+ """Open the HtmlGraph dashboard in your default browser.
18
+
19
+ Usage:
20
+ htmlgraph browse # Open dashboard
21
+ htmlgraph browse --port 8080 # Custom port
22
+ htmlgraph browse --query-type feature # Show only features
23
+ htmlgraph browse --query-status todo # Show only todo items
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ port: int = 8080,
30
+ query_type: str | None = None,
31
+ query_status: str | None = None,
32
+ ) -> None:
33
+ """Initialize BrowseCommand.
34
+
35
+ Args:
36
+ port: Server port (default: 8080)
37
+ query_type: Filter by type (feature, track, bug, spike, chore, epic)
38
+ query_status: Filter by status (todo, in_progress, blocked, done)
39
+ """
40
+ super().__init__()
41
+ self.port = port
42
+ self.query_type = query_type
43
+ self.query_status = query_status
44
+
45
+ @classmethod
46
+ def from_args(cls, args: argparse.Namespace) -> BrowseCommand:
47
+ """Create BrowseCommand from argparse arguments.
48
+
49
+ Args:
50
+ args: Argparse namespace with command arguments
51
+
52
+ Returns:
53
+ BrowseCommand instance
54
+ """
55
+ return cls(
56
+ port=args.port,
57
+ query_type=args.query_type,
58
+ query_status=args.query_status,
59
+ )
60
+
61
+ def execute(self) -> CommandResult:
62
+ """Execute the browse command.
63
+
64
+ Opens the dashboard in the default browser with optional query parameters.
65
+
66
+ Returns:
67
+ CommandResult with success status and URL
68
+ """
69
+ # Build URL with query params
70
+ url = f"http://localhost:{self.port}"
71
+
72
+ params = []
73
+ if self.query_type:
74
+ params.append(f"type={self.query_type}")
75
+ if self.query_status:
76
+ params.append(f"status={self.query_status}")
77
+
78
+ if params:
79
+ url += "?" + "&".join(params)
80
+
81
+ # Check if server is running
82
+ try:
83
+ import requests # type: ignore[import-untyped]
84
+
85
+ response = requests.head(f"http://localhost:{self.port}", timeout=1)
86
+ response.raise_for_status()
87
+ except ImportError:
88
+ # requests module not available - try to open anyway with a warning
89
+ webbrowser.open(url)
90
+ return CommandResult(
91
+ data={"url": url},
92
+ text=f"Opening dashboard at {url}\n(Note: Could not verify server is running - install 'requests' for server checks)",
93
+ exit_code=0,
94
+ )
95
+ except Exception:
96
+ # Server not running or not responding
97
+ return CommandResult(
98
+ text=f"Dashboard server not running on port {self.port}.\nStart with: htmlgraph serve --port {self.port}",
99
+ exit_code=1,
100
+ )
101
+
102
+ # Open browser
103
+ try:
104
+ webbrowser.open(url)
105
+ except Exception as e:
106
+ return CommandResult(
107
+ text=f"Failed to open browser: {e}\nYou can manually visit: {url}",
108
+ exit_code=1,
109
+ )
110
+
111
+ return CommandResult(
112
+ data={"url": url},
113
+ text=f"Opening dashboard at {url}",
114
+ exit_code=0,
115
+ )
@@ -0,0 +1,568 @@
1
+ from __future__ import annotations
2
+
3
+ """HtmlGraph CLI - Feature management commands."""
4
+
5
+
6
+ import argparse
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
15
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
16
+
17
+ if TYPE_CHECKING:
18
+ from argparse import _SubParsersAction
19
+
20
+ console = Console()
21
+
22
+
23
+ def register_feature_commands(subparsers: _SubParsersAction) -> None:
24
+ """Register feature management commands."""
25
+ feature_parser = subparsers.add_parser("feature", help="Feature management")
26
+ feature_subparsers = feature_parser.add_subparsers(
27
+ dest="feature_command", help="Feature command"
28
+ )
29
+
30
+ # feature list
31
+ feature_list = feature_subparsers.add_parser("list", help="List all features")
32
+ feature_list.add_argument(
33
+ "--status",
34
+ choices=["todo", "in_progress", "completed", "blocked"],
35
+ help="Filter by status",
36
+ )
37
+ feature_list.add_argument(
38
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
39
+ )
40
+ feature_list.add_argument(
41
+ "--format", choices=["json", "text"], default="text", help="Output format"
42
+ )
43
+ feature_list.add_argument(
44
+ "--quiet", "-q", action="store_true", help="Suppress empty output"
45
+ )
46
+ feature_list.set_defaults(func=FeatureListCommand.from_args)
47
+
48
+ # feature create
49
+ feature_create = feature_subparsers.add_parser(
50
+ "create", help="Create a new feature"
51
+ )
52
+ feature_create.add_argument("title", help="Feature title")
53
+ feature_create.add_argument("--description", help="Feature description")
54
+ feature_create.add_argument(
55
+ "--priority", choices=["low", "medium", "high", "critical"], default="medium"
56
+ )
57
+ feature_create.add_argument("--steps", type=int, help="Number of steps")
58
+ feature_create.add_argument(
59
+ "--collection", default="features", help="Collection name"
60
+ )
61
+ feature_create.add_argument("--track", help="Track ID to link feature to")
62
+ feature_create.add_argument("--agent", default="claude-code", help="Agent name")
63
+ feature_create.add_argument(
64
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
65
+ )
66
+ feature_create.add_argument(
67
+ "--format", choices=["json", "text"], default="text", help="Output format"
68
+ )
69
+ feature_create.set_defaults(func=FeatureCreateCommand.from_args)
70
+
71
+ # feature start
72
+ feature_start = feature_subparsers.add_parser(
73
+ "start", help="Start working on a feature"
74
+ )
75
+ feature_start.add_argument("id", help="Feature ID")
76
+ feature_start.add_argument(
77
+ "--collection", default="features", help="Collection name"
78
+ )
79
+ feature_start.add_argument("--agent", default="claude-code", help="Agent name")
80
+ feature_start.add_argument(
81
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
82
+ )
83
+ feature_start.add_argument(
84
+ "--format", choices=["json", "text"], default="text", help="Output format"
85
+ )
86
+ feature_start.set_defaults(func=FeatureStartCommand.from_args)
87
+
88
+ # feature complete
89
+ feature_complete = feature_subparsers.add_parser(
90
+ "complete", help="Mark feature as completed"
91
+ )
92
+ feature_complete.add_argument("id", help="Feature ID")
93
+ feature_complete.add_argument(
94
+ "--collection", default="features", help="Collection name"
95
+ )
96
+ feature_complete.add_argument("--agent", default="claude-code", help="Agent name")
97
+ feature_complete.add_argument(
98
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
99
+ )
100
+ feature_complete.add_argument(
101
+ "--format", choices=["json", "text"], default="text", help="Output format"
102
+ )
103
+ feature_complete.set_defaults(func=FeatureCompleteCommand.from_args)
104
+
105
+ # feature claim
106
+ feature_claim = feature_subparsers.add_parser("claim", help="Claim a feature")
107
+ feature_claim.add_argument("id", help="Feature ID")
108
+ feature_claim.add_argument(
109
+ "--collection", default="features", help="Collection name"
110
+ )
111
+ feature_claim.add_argument("--agent", default="claude-code", help="Agent name")
112
+ feature_claim.add_argument(
113
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
114
+ )
115
+ feature_claim.add_argument(
116
+ "--format", choices=["json", "text"], default="text", help="Output format"
117
+ )
118
+ feature_claim.set_defaults(func=FeatureClaimCommand.from_args)
119
+
120
+ # feature release
121
+ feature_release = feature_subparsers.add_parser("release", help="Release a feature")
122
+ feature_release.add_argument("id", help="Feature ID")
123
+ feature_release.add_argument(
124
+ "--collection", default="features", help="Collection name"
125
+ )
126
+ feature_release.add_argument("--agent", default="claude-code", help="Agent name")
127
+ feature_release.add_argument(
128
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
129
+ )
130
+ feature_release.add_argument(
131
+ "--format", choices=["json", "text"], default="text", help="Output format"
132
+ )
133
+ feature_release.set_defaults(func=FeatureReleaseCommand.from_args)
134
+
135
+ # feature primary
136
+ feature_primary = feature_subparsers.add_parser(
137
+ "primary", help="Set primary feature"
138
+ )
139
+ feature_primary.add_argument("id", help="Feature ID")
140
+ feature_primary.add_argument(
141
+ "--collection", default="features", help="Collection name"
142
+ )
143
+ feature_primary.add_argument("--agent", default="claude-code", help="Agent name")
144
+ feature_primary.add_argument(
145
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
146
+ )
147
+ feature_primary.add_argument(
148
+ "--format", choices=["json", "text"], default="text", help="Output format"
149
+ )
150
+ feature_primary.set_defaults(func=FeaturePrimaryCommand.from_args)
151
+
152
+
153
+ # ============================================================================
154
+ # Feature Commands
155
+ # ============================================================================
156
+
157
+
158
+ class FeatureListCommand(BaseCommand):
159
+ """List all features."""
160
+
161
+ def __init__(self, *, status: str | None, quiet: bool) -> None:
162
+ super().__init__()
163
+ self.status = status
164
+ self.quiet = quiet
165
+
166
+ @classmethod
167
+ def from_args(cls, args: argparse.Namespace) -> FeatureListCommand:
168
+ # Validate inputs using FeatureFilter model
169
+ from htmlgraph.cli.models import FeatureFilter
170
+
171
+ try:
172
+ filter_model = FeatureFilter(status=args.status)
173
+ except ValueError as e:
174
+ raise CommandError(str(e))
175
+
176
+ return cls(
177
+ status=filter_model.status,
178
+ quiet=args.quiet,
179
+ )
180
+
181
+ def execute(self) -> CommandResult:
182
+ """List all features."""
183
+ from htmlgraph.cli.models import FeatureDisplay
184
+ from htmlgraph.converter import node_to_dict
185
+
186
+ sdk = self.get_sdk()
187
+
188
+ # Query features with SDK
189
+ if self.status:
190
+ nodes = sdk.features.where(status=self.status)
191
+ else:
192
+ nodes = sdk.features.all()
193
+
194
+ # Convert to display models for type-safe sorting
195
+ display_features = [FeatureDisplay.from_node(n) for n in nodes]
196
+
197
+ # Sort by priority then updated using display model's sort_key
198
+ display_features.sort(key=lambda f: f.sort_key(), reverse=True)
199
+
200
+ if not display_features:
201
+ if not self.quiet:
202
+ from htmlgraph.cli.base import TextOutputBuilder
203
+
204
+ status_msg = f"with status '{self.status}'" if self.status else ""
205
+ output = TextOutputBuilder()
206
+ output.add_warning(f"No features found {status_msg}.")
207
+ return CommandResult(text=output.build(), json_data={"features": []})
208
+ return CommandResult(json_data={"features": []})
209
+
210
+ # Create Rich table
211
+ table = Table(
212
+ title="Features",
213
+ show_header=True,
214
+ header_style="bold magenta",
215
+ box=box.ROUNDED,
216
+ )
217
+ table.add_column("ID", style="cyan", no_wrap=False, max_width=20)
218
+ table.add_column("Title", style="yellow", max_width=40)
219
+ table.add_column("Status", style="green", width=12)
220
+ table.add_column("Priority", style="blue", width=10)
221
+ table.add_column("Updated", style="white", width=16)
222
+
223
+ for feature in display_features:
224
+ table.add_row(
225
+ feature.id,
226
+ feature.title,
227
+ feature.status,
228
+ feature.priority,
229
+ feature.updated_str,
230
+ )
231
+
232
+ # Return table object directly - TextFormatter will print it properly
233
+ return CommandResult(
234
+ data=table,
235
+ json_data=[node_to_dict(n) for n in nodes],
236
+ )
237
+
238
+
239
+ class FeatureCreateCommand(BaseCommand):
240
+ """Create a new feature."""
241
+
242
+ def __init__(
243
+ self,
244
+ *,
245
+ title: str,
246
+ description: str | None,
247
+ priority: str,
248
+ steps: int | None,
249
+ collection: str,
250
+ track_id: str | None,
251
+ ) -> None:
252
+ super().__init__()
253
+ self.title = title
254
+ self.description = description
255
+ self.priority = priority
256
+ self.steps = steps
257
+ self.collection = collection
258
+ self.track_id = track_id
259
+
260
+ @classmethod
261
+ def from_args(cls, args: argparse.Namespace) -> FeatureCreateCommand:
262
+ return cls(
263
+ title=args.title,
264
+ description=args.description,
265
+ priority=args.priority,
266
+ steps=args.steps,
267
+ collection=args.collection,
268
+ track_id=args.track,
269
+ )
270
+
271
+ def execute(self) -> CommandResult:
272
+ """Create a new feature."""
273
+ from rich.prompt import Prompt
274
+
275
+ from htmlgraph.converter import node_to_dict
276
+
277
+ sdk = self.get_sdk()
278
+
279
+ # Convert steps count to list of step names
280
+ step_names = None
281
+ if self.steps:
282
+ step_names = [f"Step {i + 1}" for i in range(self.steps)]
283
+
284
+ # Determine track_id for feature creation
285
+ track_id = self.track_id
286
+
287
+ # Only enforce track selection for main features collection
288
+ if self.collection == "features":
289
+ if not track_id:
290
+ # Get available tracks
291
+ try:
292
+ tracks = sdk.tracks.all()
293
+ if not tracks:
294
+ raise CommandError(
295
+ "No tracks found. Create a track first:\n"
296
+ " uv run htmlgraph track new 'Track Title'"
297
+ )
298
+
299
+ if len(tracks) == 1:
300
+ # Auto-select if only one track exists
301
+ track_id = tracks[0].id
302
+ console.print(
303
+ f"[dim]Auto-selected track: {tracks[0].title}[/dim]"
304
+ )
305
+ else:
306
+ # Interactive selection
307
+ console.print("[bold]Available Tracks:[/bold]")
308
+ for i, track in enumerate(tracks, 1):
309
+ console.print(f" {i}. {track.title} ({track.id})")
310
+
311
+ selection = Prompt.ask(
312
+ "Select track",
313
+ choices=[str(i) for i in range(1, len(tracks) + 1)],
314
+ )
315
+ track_id = tracks[int(selection) - 1].id
316
+ except Exception as e:
317
+ raise CommandError(f"Failed to get available tracks: {e}")
318
+
319
+ builder = sdk.features.create(
320
+ title=self.title,
321
+ description=self.description or "",
322
+ priority=self.priority,
323
+ )
324
+ if step_names:
325
+ builder.add_steps(step_names)
326
+ if track_id:
327
+ builder.set_track(track_id)
328
+ node = builder.save()
329
+ else:
330
+ node = sdk.session_manager.create_feature(
331
+ title=self.title,
332
+ collection=self.collection,
333
+ description=self.description or "",
334
+ priority=self.priority,
335
+ steps=step_names,
336
+ agent=self.agent,
337
+ )
338
+
339
+ # Create Rich table for output
340
+ table = Table(show_header=False, box=None)
341
+ table.add_column(style="bold cyan")
342
+ table.add_column()
343
+
344
+ table.add_row("Created:", f"[green]{node.id}[/green]")
345
+ table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
346
+ table.add_row("Status:", f"[blue]{node.status}[/blue]")
347
+ if node.track_id:
348
+ table.add_row("Track:", f"[cyan]{node.track_id}[/cyan]")
349
+ table.add_row(
350
+ "Path:", f"[dim]{self.graph_dir}/{self.collection}/{node.id}.html[/dim]"
351
+ )
352
+
353
+ # Return table object directly - TextFormatter will print it properly
354
+ return CommandResult(
355
+ data=table,
356
+ json_data=node_to_dict(node),
357
+ )
358
+
359
+
360
+ class FeatureStartCommand(BaseCommand):
361
+ """Start working on a feature."""
362
+
363
+ def __init__(self, *, feature_id: str, collection: str) -> None:
364
+ super().__init__()
365
+ self.feature_id = feature_id
366
+ self.collection = collection
367
+
368
+ @classmethod
369
+ def from_args(cls, args: argparse.Namespace) -> FeatureStartCommand:
370
+ return cls(feature_id=args.id, collection=args.collection)
371
+
372
+ def execute(self) -> CommandResult:
373
+ """Start working on a feature."""
374
+ from htmlgraph.converter import node_to_dict
375
+
376
+ sdk = self.get_sdk()
377
+ collection = getattr(sdk, self.collection, None)
378
+ self.require_collection(collection, self.collection)
379
+ assert collection is not None # Type narrowing for mypy
380
+
381
+ node = collection.start(self.feature_id)
382
+ self.require_node(node, "feature", self.feature_id)
383
+
384
+ status = sdk.session_manager.get_status()
385
+
386
+ # Create Rich table for output
387
+ table = Table(show_header=False, box=None)
388
+ table.add_column(style="bold cyan")
389
+ table.add_column()
390
+
391
+ table.add_row("Started:", f"[green]{node.id}[/green]")
392
+ table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
393
+ table.add_row("Status:", f"[blue]{node.status}[/blue]")
394
+ wip_color = "red" if status["wip_count"] >= status["wip_limit"] else "green"
395
+ table.add_row(
396
+ "WIP:",
397
+ f"[{wip_color}]{status['wip_count']}/{status['wip_limit']}[/{wip_color}]",
398
+ )
399
+
400
+ # Return table object directly - TextFormatter will print it properly
401
+ return CommandResult(
402
+ data=table,
403
+ json_data=node_to_dict(node),
404
+ )
405
+
406
+
407
+ class FeatureCompleteCommand(BaseCommand):
408
+ """Mark feature as completed."""
409
+
410
+ def __init__(self, *, feature_id: str, collection: str) -> None:
411
+ super().__init__()
412
+ self.feature_id = feature_id
413
+ self.collection = collection
414
+
415
+ @classmethod
416
+ def from_args(cls, args: argparse.Namespace) -> FeatureCompleteCommand:
417
+ return cls(feature_id=args.id, collection=args.collection)
418
+
419
+ def execute(self) -> CommandResult:
420
+ """Mark feature as completed."""
421
+ from htmlgraph.converter import node_to_dict
422
+
423
+ sdk = self.get_sdk()
424
+ collection = getattr(sdk, self.collection, None)
425
+ self.require_collection(collection, self.collection)
426
+ assert collection is not None # Type narrowing for mypy
427
+
428
+ node = collection.complete(self.feature_id)
429
+ self.require_node(node, "feature", self.feature_id)
430
+
431
+ # Create Rich panel for output
432
+ panel = Panel(
433
+ f"[bold green]✓ Completed[/bold green]\n"
434
+ f"[cyan]{node.id}[/cyan]\n"
435
+ f"[yellow]{node.title}[/yellow]",
436
+ border_style="green",
437
+ )
438
+
439
+ # Return panel object directly - TextFormatter will print it properly
440
+ return CommandResult(
441
+ data=panel,
442
+ json_data=node_to_dict(node),
443
+ )
444
+
445
+
446
+ class FeatureClaimCommand(BaseCommand):
447
+ """Claim a feature."""
448
+
449
+ def __init__(self, *, feature_id: str, collection: str) -> None:
450
+ super().__init__()
451
+ self.feature_id = feature_id
452
+ self.collection = collection
453
+
454
+ @classmethod
455
+ def from_args(cls, args: argparse.Namespace) -> FeatureClaimCommand:
456
+ return cls(feature_id=args.id, collection=args.collection)
457
+
458
+ def execute(self) -> CommandResult:
459
+ """Claim a feature."""
460
+ from htmlgraph.converter import node_to_dict
461
+
462
+ sdk = self.get_sdk()
463
+ collection = getattr(sdk, self.collection, None)
464
+ self.require_collection(collection, self.collection)
465
+ assert collection is not None # Type narrowing for mypy
466
+
467
+ try:
468
+ node = collection.claim(self.feature_id)
469
+ except ValueError as e:
470
+ raise CommandError(str(e))
471
+
472
+ self.require_node(node, "feature", self.feature_id)
473
+
474
+ from htmlgraph.cli.base import TextOutputBuilder
475
+
476
+ output = TextOutputBuilder()
477
+ output.add_success(f"Claimed: {node.id}")
478
+ output.add_field("Agent", node.agent_assigned)
479
+ output.add_field("Session", node.claimed_by_session)
480
+
481
+ return CommandResult(
482
+ data=node_to_dict(node),
483
+ text=output.build(),
484
+ json_data=node_to_dict(node),
485
+ )
486
+
487
+
488
+ class FeatureReleaseCommand(BaseCommand):
489
+ """Release a feature."""
490
+
491
+ def __init__(self, *, feature_id: str, collection: str) -> None:
492
+ super().__init__()
493
+ self.feature_id = feature_id
494
+ self.collection = collection
495
+
496
+ @classmethod
497
+ def from_args(cls, args: argparse.Namespace) -> FeatureReleaseCommand:
498
+ return cls(feature_id=args.id, collection=args.collection)
499
+
500
+ def execute(self) -> CommandResult:
501
+ """Release a feature."""
502
+ from htmlgraph.converter import node_to_dict
503
+
504
+ sdk = self.get_sdk()
505
+ collection = getattr(sdk, self.collection, None)
506
+ self.require_collection(collection, self.collection)
507
+ assert collection is not None # Type narrowing for mypy
508
+
509
+ try:
510
+ node = collection.release(self.feature_id)
511
+ except ValueError as e:
512
+ raise CommandError(str(e))
513
+
514
+ self.require_node(node, "feature", self.feature_id)
515
+
516
+ from htmlgraph.cli.base import TextOutputBuilder
517
+
518
+ output = TextOutputBuilder()
519
+ output.add_success(f"Released: {node.id}")
520
+
521
+ return CommandResult(
522
+ data=node_to_dict(node),
523
+ text=output.build(),
524
+ json_data=node_to_dict(node),
525
+ )
526
+
527
+
528
+ class FeaturePrimaryCommand(BaseCommand):
529
+ """Set primary feature."""
530
+
531
+ def __init__(self, *, feature_id: str, collection: str) -> None:
532
+ super().__init__()
533
+ self.feature_id = feature_id
534
+ self.collection = collection
535
+
536
+ @classmethod
537
+ def from_args(cls, args: argparse.Namespace) -> FeaturePrimaryCommand:
538
+ return cls(feature_id=args.id, collection=args.collection)
539
+
540
+ def execute(self) -> CommandResult:
541
+ """Set primary feature."""
542
+ from htmlgraph.converter import node_to_dict
543
+
544
+ sdk = self.get_sdk()
545
+
546
+ # Only FeatureCollection has set_primary currently
547
+ if self.collection == "features":
548
+ node = sdk.features.set_primary(self.feature_id)
549
+ else:
550
+ # Fallback to direct session manager
551
+ node = sdk.session_manager.set_primary_feature(
552
+ self.feature_id, collection=self.collection, agent=self.agent
553
+ )
554
+
555
+ self.require_node(node, "feature", self.feature_id)
556
+ assert node is not None # Type narrowing for mypy
557
+
558
+ from htmlgraph.cli.base import TextOutputBuilder
559
+
560
+ output = TextOutputBuilder()
561
+ output.add_success(f"Primary feature set: {node.id}")
562
+ output.add_field("Title", node.title)
563
+
564
+ return CommandResult(
565
+ data=node_to_dict(node),
566
+ text=output.build(),
567
+ json_data=node_to_dict(node),
568
+ )