htmlgraph 0.26.5__py3-none-any.whl → 0.26.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +355 -26
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/subagent_stop.py +71 -12
  38. htmlgraph/hooks/validator.py +192 -79
  39. htmlgraph/operations/__init__.py +18 -0
  40. htmlgraph/operations/initialization.py +596 -0
  41. htmlgraph/operations/initialization.py.backup +228 -0
  42. htmlgraph/orchestration/__init__.py +16 -1
  43. htmlgraph/orchestration/claude_launcher.py +185 -0
  44. htmlgraph/orchestration/command_builder.py +71 -0
  45. htmlgraph/orchestration/headless_spawner.py +72 -1332
  46. htmlgraph/orchestration/plugin_manager.py +136 -0
  47. htmlgraph/orchestration/prompts.py +137 -0
  48. htmlgraph/orchestration/spawners/__init__.py +16 -0
  49. htmlgraph/orchestration/spawners/base.py +194 -0
  50. htmlgraph/orchestration/spawners/claude.py +170 -0
  51. htmlgraph/orchestration/spawners/codex.py +442 -0
  52. htmlgraph/orchestration/spawners/copilot.py +299 -0
  53. htmlgraph/orchestration/spawners/gemini.py +478 -0
  54. htmlgraph/orchestration/subprocess_runner.py +33 -0
  55. htmlgraph/orchestration.md +563 -0
  56. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  57. htmlgraph/orchestrator_config.py +357 -0
  58. htmlgraph/orchestrator_mode.py +45 -12
  59. htmlgraph/transcript.py +16 -4
  60. htmlgraph-0.26.7.data/data/htmlgraph/dashboard.html +6592 -0
  61. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/METADATA +1 -1
  62. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/RECORD +68 -34
  63. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/entry_points.txt +1 -1
  64. htmlgraph/cli.py +0 -7256
  65. htmlgraph-0.26.5.data/data/htmlgraph/dashboard.html +0 -812
  66. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/styles.css +0 -0
  67. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  68. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  69. {htmlgraph-0.26.5.data → htmlgraph-0.26.7.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  70. {htmlgraph-0.26.5.dist-info → htmlgraph-0.26.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,485 @@
1
+ """HtmlGraph CLI - Track management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Literal, cast
8
+
9
+ from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
10
+ from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
11
+
12
+ if TYPE_CHECKING:
13
+ from argparse import _SubParsersAction
14
+
15
+
16
+ def register_track_commands(subparsers: _SubParsersAction) -> None:
17
+ """Register track management commands."""
18
+ track_parser = subparsers.add_parser("track", help="Track management")
19
+ track_subparsers = track_parser.add_subparsers(
20
+ dest="track_command", help="Track command"
21
+ )
22
+
23
+ # track new
24
+ track_new = track_subparsers.add_parser("new", help="Create a new track")
25
+ track_new.add_argument("title", help="Track title")
26
+ track_new.add_argument("--description", help="Track description")
27
+ track_new.add_argument(
28
+ "--priority", choices=["low", "medium", "high"], default="medium"
29
+ )
30
+ track_new.add_argument(
31
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
32
+ )
33
+ track_new.add_argument(
34
+ "--format", choices=["json", "text"], default="text", help="Output format"
35
+ )
36
+ track_new.set_defaults(func=TrackNewCommand.from_args)
37
+
38
+ # track list
39
+ track_list = track_subparsers.add_parser("list", help="List all tracks")
40
+ track_list.add_argument(
41
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
42
+ )
43
+ track_list.add_argument(
44
+ "--format", choices=["json", "text"], default="text", help="Output format"
45
+ )
46
+ track_list.set_defaults(func=TrackListCommand.from_args)
47
+
48
+ # track spec
49
+ track_spec = track_subparsers.add_parser("spec", help="Create track spec")
50
+ track_spec.add_argument("track_id", help="Track ID")
51
+ track_spec.add_argument("title", help="Spec title")
52
+ track_spec.add_argument("--overview", help="Spec overview")
53
+ track_spec.add_argument("--context", help="Spec context")
54
+ track_spec.add_argument("--author", help="Spec author")
55
+ track_spec.add_argument(
56
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
57
+ )
58
+ track_spec.add_argument(
59
+ "--format", choices=["json", "text"], default="text", help="Output format"
60
+ )
61
+ track_spec.set_defaults(func=TrackSpecCommand.from_args)
62
+
63
+ # track plan
64
+ track_plan = track_subparsers.add_parser("plan", help="Create track plan")
65
+ track_plan.add_argument("track_id", help="Track ID")
66
+ track_plan.add_argument("title", help="Plan title")
67
+ track_plan.add_argument(
68
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
69
+ )
70
+ track_plan.add_argument(
71
+ "--format", choices=["json", "text"], default="text", help="Output format"
72
+ )
73
+ track_plan.set_defaults(func=TrackPlanCommand.from_args)
74
+
75
+ # track delete
76
+ track_delete = track_subparsers.add_parser("delete", help="Delete a track")
77
+ track_delete.add_argument("track_id", help="Track ID")
78
+ track_delete.add_argument(
79
+ "--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
80
+ )
81
+ track_delete.add_argument(
82
+ "--format", choices=["json", "text"], default="text", help="Output format"
83
+ )
84
+ track_delete.set_defaults(func=TrackDeleteCommand.from_args)
85
+
86
+
87
+ # ============================================================================
88
+ # Track Commands
89
+ # ============================================================================
90
+
91
+
92
+ class TrackNewCommand(BaseCommand):
93
+ """Create a new track."""
94
+
95
+ def __init__(
96
+ self,
97
+ *,
98
+ title: str,
99
+ description: str | None,
100
+ priority: str,
101
+ ) -> None:
102
+ super().__init__()
103
+ self.title = title
104
+ self.description = description
105
+ self.priority = priority
106
+
107
+ @classmethod
108
+ def from_args(cls, args: argparse.Namespace) -> TrackNewCommand:
109
+ return cls(
110
+ title=args.title, description=args.description, priority=args.priority
111
+ )
112
+
113
+ def execute(self) -> CommandResult:
114
+ """Create a new track."""
115
+ from htmlgraph.track_manager import TrackManager
116
+
117
+ if self.graph_dir is None:
118
+ raise CommandError("Missing graph directory")
119
+
120
+ manager = TrackManager(self.graph_dir)
121
+
122
+ # Type cast priority to expected literal type
123
+ priority_typed = cast(
124
+ Literal["low", "medium", "high", "critical"],
125
+ self.priority,
126
+ )
127
+
128
+ try:
129
+ track = manager.create_track(
130
+ title=self.title,
131
+ description=self.description or "",
132
+ priority=priority_typed,
133
+ )
134
+ except ValueError as e:
135
+ raise CommandError(str(e))
136
+
137
+ from htmlgraph.cli.base import TextOutputBuilder
138
+
139
+ output = TextOutputBuilder()
140
+ output.add_success(f"Created track: {track.id}")
141
+ output.add_field("Title", track.title)
142
+ output.add_field("Status", track.status)
143
+ output.add_field("Priority", track.priority)
144
+ output.add_field("Path", f"{self.graph_dir}/tracks/{track.id}/")
145
+ output.add_blank()
146
+ output.add_line("Next steps:")
147
+ output.add_field(
148
+ "- Create spec", f"htmlgraph track spec {track.id} 'Spec Title'"
149
+ )
150
+ output.add_field(
151
+ "- Create plan", f"htmlgraph track plan {track.id} 'Plan Title'"
152
+ )
153
+
154
+ json_data = {
155
+ "id": track.id,
156
+ "title": track.title,
157
+ "status": track.status,
158
+ "priority": track.priority,
159
+ "path": f"{self.graph_dir}/tracks/{track.id}/",
160
+ }
161
+
162
+ return CommandResult(
163
+ data=track,
164
+ text=output.build(),
165
+ json_data=json_data,
166
+ )
167
+
168
+
169
+ class TrackListCommand(BaseCommand):
170
+ """List all tracks."""
171
+
172
+ def __init__(
173
+ self,
174
+ *,
175
+ status: str | None = None,
176
+ priority: str | None = None,
177
+ has_spec: bool | None = None,
178
+ has_plan: bool | None = None,
179
+ ) -> None:
180
+ super().__init__()
181
+ self.status = status
182
+ self.priority = priority
183
+ self.has_spec = has_spec
184
+ self.has_plan = has_plan
185
+
186
+ @classmethod
187
+ def from_args(cls, args: argparse.Namespace) -> TrackListCommand:
188
+ # Validate inputs using TrackFilter model
189
+ from htmlgraph.cli.models import TrackFilter
190
+
191
+ # Get optional filter arguments
192
+ status = getattr(args, "status", None)
193
+ priority = getattr(args, "priority", None)
194
+ has_spec = getattr(args, "has_spec", None)
195
+ has_plan = getattr(args, "has_plan", None)
196
+
197
+ try:
198
+ filter_model = TrackFilter(
199
+ status=status, priority=priority, has_spec=has_spec, has_plan=has_plan
200
+ )
201
+ except ValueError as e:
202
+ raise CommandError(str(e))
203
+
204
+ return cls(
205
+ status=filter_model.status,
206
+ priority=filter_model.priority,
207
+ has_spec=filter_model.has_spec,
208
+ has_plan=filter_model.has_plan,
209
+ )
210
+
211
+ def execute(self) -> CommandResult:
212
+ """List all tracks."""
213
+ from htmlgraph.track_manager import TrackManager
214
+
215
+ if self.graph_dir is None:
216
+ raise CommandError("Missing graph directory")
217
+
218
+ manager = TrackManager(self.graph_dir)
219
+ track_ids = manager.list_tracks()
220
+
221
+ if not track_ids:
222
+ from htmlgraph.cli.base import TextOutputBuilder
223
+
224
+ output = TextOutputBuilder()
225
+ output.add_warning("No tracks found.")
226
+ output.add_blank()
227
+ output.add_dim("Create a track with: htmlgraph track new 'Track Title'")
228
+
229
+ return CommandResult(
230
+ text=output.build(),
231
+ json_data={"tracks": []},
232
+ )
233
+
234
+ # Create Rich table
235
+ from htmlgraph.cli.base import TableBuilder
236
+
237
+ builder = TableBuilder.create_list_table(f"Tracks in {self.graph_dir}/tracks/")
238
+ builder.add_id_column("Track ID", no_wrap=True)
239
+ builder.add_column("Components", style="green")
240
+ builder.add_column("Format", style="blue")
241
+
242
+ # Convert to display models for type-safe filtering
243
+ from htmlgraph.cli.models import TrackDisplay
244
+
245
+ display_tracks = []
246
+
247
+ for track_id in track_ids:
248
+ # Check for both consolidated and directory-based formats
249
+ track_file = Path(self.graph_dir) / "tracks" / f"{track_id}.html"
250
+ track_dir = Path(self.graph_dir) / "tracks" / track_id
251
+
252
+ if track_file.exists():
253
+ # Consolidated format
254
+ content = track_file.read_text(encoding="utf-8")
255
+ has_spec = (
256
+ 'data-section="overview"' in content
257
+ or 'data-section="requirements"' in content
258
+ )
259
+ has_plan = 'data-section="plan"' in content
260
+ format_type = "consolidated"
261
+ else:
262
+ # Directory format
263
+ has_spec = (track_dir / "spec.html").exists()
264
+ has_plan = (track_dir / "plan.html").exists()
265
+ format_type = "directory"
266
+
267
+ # Create display model
268
+ track_display = TrackDisplay.from_track_id(
269
+ track_id=track_id,
270
+ has_spec=has_spec,
271
+ has_plan=has_plan,
272
+ format_type=format_type,
273
+ )
274
+
275
+ # Apply filters
276
+ if self.has_spec is not None and track_display.has_spec != self.has_spec:
277
+ continue
278
+ if self.has_plan is not None and track_display.has_plan != self.has_plan:
279
+ continue
280
+
281
+ display_tracks.append(track_display)
282
+
283
+ for track in display_tracks:
284
+ builder.add_row(track.id, track.components_str, track.format_type)
285
+
286
+ # Return table object directly - TextFormatter will print it properly
287
+ return CommandResult(
288
+ data=builder.table,
289
+ json_data={"tracks": track_ids},
290
+ )
291
+
292
+
293
+ class TrackSpecCommand(BaseCommand):
294
+ """Create track spec."""
295
+
296
+ def __init__(
297
+ self,
298
+ *,
299
+ track_id: str,
300
+ title: str,
301
+ overview: str | None,
302
+ context: str | None,
303
+ author: str | None,
304
+ ) -> None:
305
+ super().__init__()
306
+ self.track_id = track_id
307
+ self.title = title
308
+ self.overview = overview
309
+ self.context = context
310
+ self.author = author
311
+
312
+ @classmethod
313
+ def from_args(cls, args: argparse.Namespace) -> TrackSpecCommand:
314
+ return cls(
315
+ track_id=args.track_id,
316
+ title=args.title,
317
+ overview=args.overview,
318
+ context=args.context,
319
+ author=args.author,
320
+ )
321
+
322
+ def execute(self) -> CommandResult:
323
+ """Create track spec."""
324
+ from htmlgraph.track_manager import TrackManager
325
+
326
+ if self.graph_dir is None:
327
+ raise CommandError("Missing graph directory")
328
+
329
+ manager = TrackManager(self.graph_dir)
330
+
331
+ # Check if track uses consolidated format
332
+ if manager.is_consolidated(self.track_id):
333
+ track_file = manager.tracks_dir / f"{self.track_id}.html"
334
+ msg = [
335
+ f"Track '{self.track_id}' uses consolidated single-file format.",
336
+ f"Spec is embedded in: {track_file}",
337
+ "\nTo create a track with separate spec/plan files, use:",
338
+ ' sdk.tracks.builder().separate_files().title("...").create()',
339
+ ]
340
+ return CommandResult(text="\n".join(msg))
341
+
342
+ try:
343
+ spec = manager.create_spec(
344
+ track_id=self.track_id,
345
+ title=self.title,
346
+ overview=self.overview or "",
347
+ context=self.context or "",
348
+ author=self.author or "",
349
+ )
350
+ except (ValueError, FileNotFoundError) as e:
351
+ raise CommandError(str(e))
352
+
353
+ from htmlgraph.cli.base import TextOutputBuilder
354
+
355
+ output = TextOutputBuilder()
356
+ output.add_success(f"Created spec: {spec.id}")
357
+ output.add_field("Title", spec.title)
358
+ output.add_field("Track", spec.track_id)
359
+ output.add_field("Status", spec.status)
360
+ output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/spec.html")
361
+ output.add_blank()
362
+ output.add_line(
363
+ f"View spec: open {self.graph_dir}/tracks/{self.track_id}/spec.html"
364
+ )
365
+
366
+ json_data = {
367
+ "id": spec.id,
368
+ "title": spec.title,
369
+ "track_id": spec.track_id,
370
+ "status": spec.status,
371
+ "path": f"{self.graph_dir}/tracks/{self.track_id}/spec.html",
372
+ }
373
+
374
+ return CommandResult(
375
+ data=spec,
376
+ text=output.build(),
377
+ json_data=json_data,
378
+ )
379
+
380
+
381
+ class TrackPlanCommand(BaseCommand):
382
+ """Create track plan."""
383
+
384
+ def __init__(self, *, track_id: str, title: str) -> None:
385
+ super().__init__()
386
+ self.track_id = track_id
387
+ self.title = title
388
+
389
+ @classmethod
390
+ def from_args(cls, args: argparse.Namespace) -> TrackPlanCommand:
391
+ return cls(track_id=args.track_id, title=args.title)
392
+
393
+ def execute(self) -> CommandResult:
394
+ """Create track plan."""
395
+ from htmlgraph.track_manager import TrackManager
396
+
397
+ if self.graph_dir is None:
398
+ raise CommandError("Missing graph directory")
399
+
400
+ manager = TrackManager(self.graph_dir)
401
+
402
+ # Check if track uses consolidated format
403
+ if manager.is_consolidated(self.track_id):
404
+ track_file = manager.tracks_dir / f"{self.track_id}.html"
405
+ msg = [
406
+ f"Track '{self.track_id}' uses consolidated single-file format.",
407
+ f"Plan is embedded in: {track_file}",
408
+ "\nTo create a track with separate spec/plan files, use:",
409
+ ' sdk.tracks.builder().separate_files().title("...").create()',
410
+ ]
411
+ return CommandResult(text="\n".join(msg))
412
+
413
+ try:
414
+ plan = manager.create_plan(
415
+ track_id=self.track_id,
416
+ title=self.title,
417
+ )
418
+ except (ValueError, FileNotFoundError) as e:
419
+ raise CommandError(str(e))
420
+
421
+ from htmlgraph.cli.base import TextOutputBuilder
422
+
423
+ output = TextOutputBuilder()
424
+ output.add_success(f"Created plan: {plan.id}")
425
+ output.add_field("Title", plan.title)
426
+ output.add_field("Track", plan.track_id)
427
+ output.add_field("Status", plan.status)
428
+ output.add_field("Path", f"{self.graph_dir}/tracks/{self.track_id}/plan.html")
429
+ output.add_blank()
430
+ output.add_line(
431
+ f"View plan: open {self.graph_dir}/tracks/{self.track_id}/plan.html"
432
+ )
433
+
434
+ json_data = {
435
+ "id": plan.id,
436
+ "title": plan.title,
437
+ "track_id": plan.track_id,
438
+ "status": plan.status,
439
+ "path": f"{self.graph_dir}/tracks/{self.track_id}/plan.html",
440
+ }
441
+
442
+ return CommandResult(
443
+ data=plan,
444
+ text=output.build(),
445
+ json_data=json_data,
446
+ )
447
+
448
+
449
+ class TrackDeleteCommand(BaseCommand):
450
+ """Delete a track."""
451
+
452
+ def __init__(self, *, track_id: str) -> None:
453
+ super().__init__()
454
+ self.track_id = track_id
455
+
456
+ @classmethod
457
+ def from_args(cls, args: argparse.Namespace) -> TrackDeleteCommand:
458
+ return cls(track_id=args.track_id)
459
+
460
+ def execute(self) -> CommandResult:
461
+ """Delete a track."""
462
+ from htmlgraph.track_manager import TrackManager
463
+
464
+ if self.graph_dir is None:
465
+ raise CommandError("Missing graph directory")
466
+
467
+ manager = TrackManager(self.graph_dir)
468
+
469
+ try:
470
+ manager.delete_track(self.track_id)
471
+ except ValueError as e:
472
+ raise CommandError(str(e))
473
+
474
+ from htmlgraph.cli.base import TextOutputBuilder
475
+
476
+ output = TextOutputBuilder()
477
+ output.add_success(f"Deleted track: {self.track_id}")
478
+ output.add_field("Removed", f"{self.graph_dir}/tracks/{self.track_id}/")
479
+
480
+ json_data = {"deleted": True, "track_id": self.track_id}
481
+
482
+ return CommandResult(
483
+ text=output.build(),
484
+ json_data=json_data,
485
+ )