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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {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))
|