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,559 @@
1
+ from __future__ import annotations
2
+
3
+ """HtmlGraph CLI - Snapshot command for graph state visualization."""
4
+
5
+
6
+ import argparse
7
+ import json
8
+ from typing import Any
9
+
10
+ from rich.console import Console
11
+
12
+ from htmlgraph.cli.base import BaseCommand, CommandResult
13
+
14
+
15
+ class SnapshotFormatter:
16
+ """Helper for agent-friendly colored output formatting.
17
+
18
+ Uses ANSI color codes that are visible to humans but harmless to agents.
19
+ Avoids box-drawing characters and complex table formatting.
20
+ """
21
+
22
+ def __init__(self) -> None:
23
+ """Initialize formatter with Rich console."""
24
+ # Force color output even when not in TTY
25
+ self.console = Console(force_terminal=True, legacy_windows=False)
26
+
27
+ def colorize_status(self, status: str) -> str:
28
+ """Return ANSI-colored status string.
29
+
30
+ Args:
31
+ status: Status value (todo, in-progress, blocked, done)
32
+
33
+ Returns:
34
+ Colored status string with Rich markup
35
+ """
36
+ colors = {
37
+ "todo": "yellow",
38
+ "in-progress": "cyan",
39
+ "blocked": "red",
40
+ "done": "green",
41
+ }
42
+ color = colors.get(status, "white")
43
+ return f"[{color}]{status}[/{color}]"
44
+
45
+ def colorize_priority(self, priority: str | None) -> str:
46
+ """Return ANSI-colored priority string.
47
+
48
+ Args:
49
+ priority: Priority value (critical, high, medium, low)
50
+
51
+ Returns:
52
+ Colored priority string with Rich markup
53
+ """
54
+ if not priority:
55
+ return "[dim]-[/dim]"
56
+
57
+ colors = {
58
+ "critical": "red",
59
+ "high": "red",
60
+ "medium": "yellow",
61
+ "low": "dim",
62
+ }
63
+ color = colors.get(priority, "white")
64
+ return f"[{color}]{priority}[/{color}]"
65
+
66
+ def colorize_ref(self, ref: str | None) -> str:
67
+ """Return ANSI-colored ref.
68
+
69
+ Args:
70
+ ref: Reference string (@f1, @t1, etc.)
71
+
72
+ Returns:
73
+ Colored ref string with Rich markup
74
+ """
75
+ if not ref:
76
+ return " "
77
+ return f"[cyan]{ref}[/cyan]"
78
+
79
+ def status_symbol(self, status: str) -> str:
80
+ """Return appropriate Unicode symbol for status.
81
+
82
+ Args:
83
+ status: Status value
84
+
85
+ Returns:
86
+ Unicode symbol representing status
87
+ """
88
+ symbols = {
89
+ "done": "✓",
90
+ "blocked": "✗",
91
+ "in-progress": "⟳",
92
+ "todo": "●",
93
+ }
94
+ return symbols.get(status, "●")
95
+
96
+ def render(self, text: str) -> str:
97
+ """Render Rich markup to ANSI-escaped string.
98
+
99
+ Args:
100
+ text: Text with Rich markup
101
+
102
+ Returns:
103
+ String with ANSI color codes
104
+ """
105
+ # Use Rich's export_text to get ANSI-formatted output
106
+ from rich.text import Text
107
+
108
+ # Parse Rich markup
109
+ rich_text = Text.from_markup(text)
110
+
111
+ # Render to string with ANSI codes
112
+ with self.console.capture() as capture:
113
+ self.console.print(rich_text, end="")
114
+ return capture.get()
115
+
116
+
117
+ class SnapshotCommand(BaseCommand):
118
+ """Generate and output a snapshot of the current graph state.
119
+
120
+ Outputs all work items organized by type and status, optionally with
121
+ short refs for AI-friendly references.
122
+
123
+ Usage:
124
+ htmlgraph snapshot # Human-readable with refs
125
+ htmlgraph snapshot --format json # JSON format
126
+ htmlgraph snapshot --format text # Simple text (no refs)
127
+ htmlgraph snapshot --type feature # Only features
128
+ htmlgraph snapshot --status todo # Only todo items
129
+ htmlgraph snapshot --active # Only active work (TODO/IN_PROGRESS)
130
+ htmlgraph snapshot --track @t1 # Only items in track
131
+ htmlgraph snapshot --blockers # Only critical/blocked items
132
+ htmlgraph snapshot --summary # Summary with counts and progress
133
+ htmlgraph snapshot --my-work # Only items assigned to current agent
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ *,
139
+ output_format: str = "refs",
140
+ node_type: str | None = None,
141
+ status: str | None = None,
142
+ track_id: str | None = None,
143
+ active: bool = False,
144
+ blockers: bool = False,
145
+ summary: bool = False,
146
+ my_work: bool = False,
147
+ ) -> None:
148
+ """Initialize snapshot command.
149
+
150
+ Args:
151
+ output_format: Output format (refs, json, text)
152
+ node_type: Filter by type (feature, track, bug, spike, chore, epic, all)
153
+ status: Filter by status (todo, in_progress, blocked, done, all)
154
+ track_id: Filter by track ID or ref
155
+ active: Show only TODO/IN_PROGRESS items
156
+ blockers: Show only critical/blocked items
157
+ summary: Show summary format with counts
158
+ my_work: Show only items assigned to current agent
159
+ """
160
+ super().__init__()
161
+ self.output_format = output_format
162
+ self.node_type = node_type
163
+ self.status = status
164
+ self.track_id = track_id
165
+ self.active = active
166
+ self.blockers = blockers
167
+ self.summary = summary
168
+ self.my_work = my_work
169
+ self.formatter = SnapshotFormatter()
170
+
171
+ @classmethod
172
+ def from_args(cls, args: argparse.Namespace) -> SnapshotCommand:
173
+ """Create command instance from argparse arguments."""
174
+ cmd = cls(
175
+ output_format=args.output_format
176
+ if hasattr(args, "output_format")
177
+ else "refs",
178
+ node_type=args.type if hasattr(args, "type") else None,
179
+ status=args.status if hasattr(args, "status") else None,
180
+ track_id=args.track if hasattr(args, "track") else None,
181
+ active=args.active if hasattr(args, "active") else False,
182
+ blockers=args.blockers if hasattr(args, "blockers") else False,
183
+ summary=args.summary if hasattr(args, "summary") else False,
184
+ my_work=args.my_work if hasattr(args, "my_work") else False,
185
+ )
186
+ # If snapshot command has its own --output-format, override the global --format
187
+ # This allows "htmlgraph snapshot --output-format json" to work without needing --format json
188
+ if hasattr(args, "output_format"):
189
+ cmd.override_output_format = args.output_format
190
+ return cmd
191
+
192
+ def execute(self) -> CommandResult:
193
+ """Execute snapshot command."""
194
+ sdk = self.get_sdk()
195
+
196
+ # Gather all work items
197
+ items = self._gather_items(sdk)
198
+
199
+ # Format output based on output_format setting
200
+ if self.summary:
201
+ output = self._format_summary(items, sdk)
202
+ return CommandResult(
203
+ json_data=items, # For JsonFormatter if needed
204
+ data={"snapshot": output, "item_count": len(items)},
205
+ text=output,
206
+ )
207
+ elif self.output_format == "json":
208
+ # For JSON format, return items as both json_data and text
209
+ # This allows both direct result.text access (in tests) and
210
+ # JsonFormatter to work correctly
211
+ json_text = self._format_json(items)
212
+ return CommandResult(
213
+ json_data=items, # For JsonFormatter
214
+ data=items, # For backward compatibility
215
+ text=json_text, # JSON string for direct access
216
+ )
217
+ elif self.output_format == "refs":
218
+ output = self._format_refs(items)
219
+ else: # text
220
+ output = self._format_text(items)
221
+
222
+ return CommandResult(
223
+ json_data=items, # For JsonFormatter if needed
224
+ data={"snapshot": output, "item_count": len(items)},
225
+ text=output,
226
+ )
227
+
228
+ def _gather_items(self, sdk: Any) -> list[dict[str, Any]]:
229
+ """Gather all relevant items from SDK.
230
+
231
+ Args:
232
+ sdk: HtmlGraph SDK instance
233
+
234
+ Returns:
235
+ List of item dicts with ref, id, type, title, status, priority
236
+ """
237
+ items = []
238
+
239
+ # Resolve track_id if provided as ref
240
+ resolved_track_id = None
241
+ if self.track_id:
242
+ if self.track_id.startswith("@"):
243
+ # Resolve ref to track ID
244
+ track_node = sdk.ref(self.track_id)
245
+ if track_node and track_node.type == "track":
246
+ resolved_track_id = track_node.id
247
+ else:
248
+ resolved_track_id = self.track_id
249
+
250
+ # Map collection names to SDK attributes
251
+ collection_map = {
252
+ "feature": "features",
253
+ "track": "tracks",
254
+ "bug": "bugs",
255
+ "spike": "spikes",
256
+ "chore": "chores",
257
+ "epic": "epics",
258
+ }
259
+
260
+ for node_type, collection_name in collection_map.items():
261
+ # Apply type filter
262
+ if (
263
+ self.node_type
264
+ and self.node_type != "all"
265
+ and self.node_type != node_type
266
+ ):
267
+ continue
268
+
269
+ # Get collection
270
+ collection = getattr(sdk, collection_name, None)
271
+ if not collection:
272
+ continue
273
+
274
+ # Get all nodes from collection
275
+ nodes = collection.all()
276
+
277
+ for node in nodes:
278
+ # Apply status filter
279
+ if self.status and self.status != "all" and node.status != self.status:
280
+ continue
281
+
282
+ # Apply active filter
283
+ if self.active:
284
+ if node.status not in ["todo", "in-progress", "blocked"]:
285
+ continue
286
+ # Filter out metadata spikes
287
+ if node.type == "spike" and self._is_metadata_spike(node):
288
+ continue
289
+
290
+ # Apply blockers filter
291
+ if self.blockers:
292
+ priority = getattr(node, "priority", None)
293
+ if priority != "critical" and node.status != "blocked":
294
+ continue
295
+
296
+ # Apply track filter
297
+ if resolved_track_id:
298
+ node_track_id = getattr(node, "track_id", None)
299
+ if node_track_id != resolved_track_id:
300
+ continue
301
+
302
+ # Apply my_work filter
303
+ if self.my_work:
304
+ assigned_to = getattr(node, "agent_assigned", None)
305
+ if assigned_to != sdk.agent:
306
+ continue
307
+
308
+ items.append(self._node_to_dict(sdk, node))
309
+
310
+ # Sort by type, status, then ref
311
+ return sorted(items, key=lambda x: (x["type"], x["status"], x["ref"] or ""))
312
+
313
+ def _is_metadata_spike(self, node: Any) -> bool:
314
+ """Check if spike is metadata (conversation, transition, etc).
315
+
316
+ Args:
317
+ node: Spike node to check
318
+
319
+ Returns:
320
+ True if spike is metadata
321
+ """
322
+ title = node.title.lower()
323
+ metadata_keywords = ["conversation", "transition", "handoff", "session"]
324
+ return any(keyword in title for keyword in metadata_keywords)
325
+
326
+ def _node_to_dict(self, sdk: Any, node: Any) -> dict[str, Any]:
327
+ """Convert Node to dict with ref.
328
+
329
+ Args:
330
+ sdk: HtmlGraph SDK instance
331
+ node: Node object
332
+
333
+ Returns:
334
+ Dict with ref, id, type, title, status, priority, assigned_to, track_id
335
+ """
336
+ # Get ref if available (may not exist yet)
337
+ ref = None
338
+ if hasattr(sdk, "refs") and sdk.refs:
339
+ ref = sdk.refs.get_ref(node.id)
340
+
341
+ return {
342
+ "ref": ref,
343
+ "id": node.id,
344
+ "type": node.type,
345
+ "title": node.title,
346
+ "status": node.status,
347
+ "priority": getattr(node, "priority", None),
348
+ "assigned_to": getattr(node, "agent_assigned", None),
349
+ "track_id": getattr(node, "track_id", None),
350
+ }
351
+
352
+ def _format_refs(self, items: list[dict]) -> str:
353
+ """Format as readable list with refs and ANSI colors.
354
+
355
+ Args:
356
+ items: List of item dicts
357
+
358
+ Returns:
359
+ Formatted string with refs and ANSI color codes
360
+ """
361
+ lines = []
362
+ lines.append("[bold]SNAPSHOT - Current Graph State[/bold]")
363
+ lines.append("=" * 50)
364
+
365
+ # Group by type
366
+ by_type: dict[str, list[dict[str, Any]]] = {}
367
+ for item in items:
368
+ t = item["type"]
369
+ if t not in by_type:
370
+ by_type[t] = []
371
+ by_type[t].append(item)
372
+
373
+ # Iterate through types in consistent order
374
+ for node_type in ["feature", "track", "bug", "spike", "chore", "epic"]:
375
+ if node_type not in by_type:
376
+ continue
377
+
378
+ type_items = by_type[node_type]
379
+ lines.append(f"\n[bold]{node_type.upper()}S ({len(type_items)})[/bold]")
380
+ lines.append("─" * 40)
381
+
382
+ # Group by status
383
+ by_status: dict[str, list[dict[str, Any]]] = {}
384
+ for item in type_items:
385
+ status = item["status"]
386
+ if status not in by_status:
387
+ by_status[status] = []
388
+ by_status[status].append(item)
389
+
390
+ # Iterate through statuses in consistent order
391
+ for status in ["todo", "in-progress", "blocked", "done"]:
392
+ if status not in by_status:
393
+ continue
394
+
395
+ lines.append(f"\n{status.upper().replace('-', '_')}:")
396
+ for item in by_status[status]:
397
+ ref = self.formatter.colorize_ref(item["ref"])
398
+ title = (
399
+ item["title"][:40] if len(item["title"]) > 40 else item["title"]
400
+ )
401
+ prio = self.formatter.colorize_priority(item["priority"])
402
+ status_colored = self.formatter.colorize_status(item["status"])
403
+ lines.append(f" {ref} {title:40s} {prio:10s} {status_colored}")
404
+
405
+ # Render all lines with Rich markup to ANSI
406
+ return self.formatter.render("\n".join(lines))
407
+
408
+ def _format_json(self, items: list[dict]) -> str:
409
+ """Format as JSON.
410
+
411
+ Args:
412
+ items: List of item dicts
413
+
414
+ Returns:
415
+ JSON string
416
+ """
417
+ return json.dumps(items, indent=2, default=str)
418
+
419
+ def _format_text(self, items: list[dict]) -> str:
420
+ """Format as simple text with colors (no refs).
421
+
422
+ Args:
423
+ items: List of item dicts
424
+
425
+ Returns:
426
+ Plain text string with ANSI color codes
427
+ """
428
+ lines = []
429
+ for item in items:
430
+ title = item["title"][:40] if len(item["title"]) > 40 else item["title"]
431
+ item_type = item["type"]
432
+ status_colored = self.formatter.colorize_status(item["status"])
433
+ lines.append(f"{item_type:8s} {title:40s} {status_colored}")
434
+ return self.formatter.render("\n".join(lines))
435
+
436
+ def _format_summary(self, items: list[dict], sdk: Any) -> str:
437
+ """Format as summary with counts, progress, colors, and symbols.
438
+
439
+ Args:
440
+ items: List of item dicts
441
+ sdk: HtmlGraph SDK instance
442
+
443
+ Returns:
444
+ Summary string with ANSI colors and Unicode symbols
445
+ """
446
+ lines = []
447
+ lines.append("[bold]ACTIVE WORK CONTEXT[/bold]")
448
+ lines.append("═" * 60)
449
+ lines.append("")
450
+
451
+ # Show current track if track filter is active
452
+ if self.track_id:
453
+ track_ref = self.track_id if self.track_id.startswith("@") else None
454
+ if not track_ref:
455
+ # Try to get ref from track_id
456
+ track_ref = sdk.refs.get_ref(self.track_id)
457
+ track_ref_colored = self.formatter.colorize_ref(track_ref or self.track_id)
458
+ lines.append(f"Current Track: {track_ref_colored}")
459
+ lines.append("")
460
+
461
+ # Group by type
462
+ by_type: dict[str, list[dict[str, Any]]] = {}
463
+ for item in items:
464
+ t = item["type"]
465
+ if t not in by_type:
466
+ by_type[t] = []
467
+ by_type[t].append(item)
468
+
469
+ # Features summary
470
+ if "feature" in by_type:
471
+ features = by_type["feature"]
472
+ done_count = sum(1 for f in features if f["status"] == "done")
473
+ total_count = len(features)
474
+ progress = int((done_count / total_count) * 100) if total_count > 0 else 0
475
+
476
+ lines.append(
477
+ f"[bold]● Active Features ({done_count}/{total_count} complete - {progress}%):[/bold]"
478
+ )
479
+ # Show active features (not done)
480
+ active_features = [f for f in features if f["status"] != "done"]
481
+ for feature in active_features[:5]: # Limit to 5
482
+ ref = self.formatter.colorize_ref(feature["ref"])
483
+ symbol = self.formatter.status_symbol(feature["status"])
484
+ title = (
485
+ feature["title"][:40]
486
+ if len(feature["title"]) > 40
487
+ else feature["title"]
488
+ )
489
+ prio = self.formatter.colorize_priority(feature["priority"])
490
+ lines.append(f" {ref} {symbol} {title:40s} {prio}")
491
+ if len(active_features) > 5:
492
+ lines.append(f" ... and {len(active_features) - 5} more")
493
+ lines.append("")
494
+
495
+ # Bugs summary
496
+ if "bug" in by_type:
497
+ bugs = by_type["bug"]
498
+ critical_bugs = [b for b in bugs if b["priority"] == "critical"]
499
+ high_bugs = [b for b in bugs if b["priority"] == "high"]
500
+
501
+ lines.append(
502
+ f"[bold]✗ Active Bugs ({len(critical_bugs)} critical, {len(high_bugs)} high):[/bold]"
503
+ )
504
+ # Show critical and high priority bugs
505
+ priority_bugs = critical_bugs + high_bugs
506
+ for bug in priority_bugs[:5]: # Limit to 5
507
+ ref = self.formatter.colorize_ref(bug["ref"])
508
+ symbol = self.formatter.status_symbol(bug["status"])
509
+ title = bug["title"][:40] if len(bug["title"]) > 40 else bug["title"]
510
+ prio = self.formatter.colorize_priority(bug["priority"])
511
+ lines.append(f" {ref} {symbol} {title:40s} {prio}")
512
+ if len(priority_bugs) > 5:
513
+ lines.append(f" ... and {len(priority_bugs) - 5} more")
514
+ lines.append("")
515
+
516
+ # Blockers & Critical summary
517
+ blockers = [
518
+ i for i in items if i["priority"] == "critical" or i["status"] == "blocked"
519
+ ]
520
+ if blockers:
521
+ lines.append(f"[bold]⚠ Blockers & Critical ({len(blockers)} items):[/bold]")
522
+ for item in blockers[:5]: # Limit to 5
523
+ ref = self.formatter.colorize_ref(item["ref"])
524
+ symbol = self.formatter.status_symbol(item["status"])
525
+ title = item["title"][:40] if len(item["title"]) > 40 else item["title"]
526
+ prio = self.formatter.colorize_priority(item["priority"])
527
+ lines.append(f" {ref} {symbol} {title:40s} {prio}")
528
+ if len(blockers) > 5:
529
+ lines.append(f" ... and {len(blockers) - 5} more")
530
+ lines.append("")
531
+
532
+ # Quick Stats
533
+ lines.append("[bold]Quick Stats:[/bold]")
534
+ if "feature" in by_type:
535
+ features = by_type["feature"]
536
+ done = sum(1 for f in features if f["status"] == "done")
537
+ total = len(features)
538
+ progress = int((done / total) * 100) if total > 0 else 0
539
+ if self.track_id:
540
+ lines.append(f" Track: {done}/{total} features ({progress}% done)")
541
+ else:
542
+ lines.append(f" Features: {done}/{total} complete ({progress}% done)")
543
+
544
+ if "bug" in by_type:
545
+ bugs = by_type["bug"]
546
+ open_bugs = sum(1 for b in bugs if b["status"] != "done")
547
+ critical = sum(1 for b in bugs if b["priority"] == "critical")
548
+ lines.append(f" Bugs: {open_bugs} open ({critical} critical)")
549
+
550
+ if "spike" in by_type:
551
+ spikes = by_type["spike"]
552
+ lines.append(f" Spikes: {len(spikes)} active")
553
+
554
+ if "track" in by_type:
555
+ tracks = by_type["track"]
556
+ active_tracks = sum(1 for t in tracks if t["status"] == "in-progress")
557
+ lines.append(f" Tracks: {active_tracks} active")
558
+
559
+ return self.formatter.render("\n".join(lines))