htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__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 (36) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/analytics/pattern_learning.py +771 -0
  3. htmlgraph/api/main.py +56 -23
  4. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  5. htmlgraph/api/templates/dashboard.html +3 -3
  6. htmlgraph/api/templates/partials/work-items.html +613 -0
  7. htmlgraph/builders/track.py +26 -0
  8. htmlgraph/cli/base.py +31 -7
  9. htmlgraph/cli/work/__init__.py +74 -0
  10. htmlgraph/cli/work/browse.py +114 -0
  11. htmlgraph/cli/work/snapshot.py +558 -0
  12. htmlgraph/collections/base.py +34 -0
  13. htmlgraph/collections/todo.py +12 -0
  14. htmlgraph/converter.py +11 -0
  15. htmlgraph/db/schema.py +34 -1
  16. htmlgraph/hooks/orchestrator.py +88 -14
  17. htmlgraph/hooks/session_handler.py +3 -1
  18. htmlgraph/models.py +22 -2
  19. htmlgraph/orchestration/__init__.py +4 -0
  20. htmlgraph/orchestration/plugin_manager.py +1 -2
  21. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  22. htmlgraph/refs.py +343 -0
  23. htmlgraph/sdk.py +162 -1
  24. htmlgraph/session_manager.py +154 -2
  25. htmlgraph/sessions/__init__.py +23 -0
  26. htmlgraph/sessions/handoff.py +755 -0
  27. htmlgraph/track_builder.py +12 -0
  28. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
  29. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
  30. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
  31. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
  32. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  33. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  34. {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  35. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
  36. {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py CHANGED
@@ -163,6 +163,12 @@ class HtmlGraphDB:
163
163
  ("completed_at", "DATETIME"),
164
164
  ("last_user_query_at", "DATETIME"),
165
165
  ("last_user_query", "TEXT"),
166
+ # Phase 2 Feature 3: Cross-Session Continuity handoff fields
167
+ ("handoff_notes", "TEXT"),
168
+ ("recommended_next", "TEXT"),
169
+ ("blockers", "TEXT"), # JSON array of blocker strings
170
+ ("recommended_context", "TEXT"), # JSON array of file paths
171
+ ("continued_from", "TEXT"), # Previous session ID
166
172
  ]
167
173
 
168
174
  # Refresh columns after potential rename
@@ -291,8 +297,14 @@ class HtmlGraphDB:
291
297
  metadata JSON,
292
298
  last_user_query_at DATETIME,
293
299
  last_user_query TEXT,
300
+ handoff_notes TEXT,
301
+ recommended_next TEXT,
302
+ blockers JSON,
303
+ recommended_context JSON,
304
+ continued_from TEXT,
294
305
  FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
295
- FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
306
+ FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
307
+ FOREIGN KEY (continued_from) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE
296
308
  )
297
309
  """)
298
310
 
@@ -407,6 +419,23 @@ class HtmlGraphDB:
407
419
  )
408
420
  """)
409
421
 
422
+ # 10. HANDOFF_TRACKING TABLE - Phase 2 Feature 3: Track handoff effectiveness
423
+ cursor.execute("""
424
+ CREATE TABLE IF NOT EXISTS handoff_tracking (
425
+ handoff_id TEXT PRIMARY KEY,
426
+ from_session_id TEXT NOT NULL,
427
+ to_session_id TEXT,
428
+ items_in_context INTEGER DEFAULT 0,
429
+ items_accessed INTEGER DEFAULT 0,
430
+ time_to_resume_seconds INTEGER DEFAULT 0,
431
+ user_rating INTEGER CHECK(user_rating BETWEEN 1 AND 5),
432
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
433
+ resumed_at DATETIME,
434
+ FOREIGN KEY (from_session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
435
+ FOREIGN KEY (to_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
436
+ )
437
+ """)
438
+
410
439
  # 9. Create indexes for performance
411
440
  self._create_indexes(cursor)
412
441
 
@@ -496,6 +525,10 @@ class HtmlGraphDB:
496
525
  # live_events indexes - optimized for real-time WebSocket streaming
497
526
  "CREATE INDEX IF NOT EXISTS idx_live_events_pending ON live_events(broadcast_at) WHERE broadcast_at IS NULL",
498
527
  "CREATE INDEX IF NOT EXISTS idx_live_events_created ON live_events(created_at DESC)",
528
+ # handoff_tracking indexes - optimized for handoff effectiveness queries
529
+ "CREATE INDEX IF NOT EXISTS idx_handoff_from_session ON handoff_tracking(from_session_id, created_at DESC)",
530
+ "CREATE INDEX IF NOT EXISTS idx_handoff_to_session ON handoff_tracking(to_session_id, resumed_at DESC)",
531
+ "CREATE INDEX IF NOT EXISTS idx_handoff_rating ON handoff_tracking(user_rating, created_at DESC)",
499
532
  ]
500
533
 
501
534
  for index_sql in indexes:
@@ -94,6 +94,73 @@ def load_tool_history(session_id: str) -> list[dict]:
94
94
  return []
95
95
 
96
96
 
97
+ def record_tool_event(tool_name: str, session_id: str) -> None:
98
+ """
99
+ Record a tool event to the database for history tracking.
100
+
101
+ This is called at the end of PreToolUse hook execution to track
102
+ tool usage patterns for sequence detection.
103
+
104
+ Args:
105
+ tool_name: Name of the tool being called
106
+ session_id: Session identifier for isolation
107
+ """
108
+ try:
109
+ import datetime
110
+ import uuid
111
+
112
+ from htmlgraph.db.schema import HtmlGraphDB
113
+
114
+ # Find database path
115
+ cwd = Path.cwd()
116
+ graph_dir = cwd / ".htmlgraph"
117
+ if not graph_dir.exists():
118
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
119
+ candidate = parent / ".htmlgraph"
120
+ if candidate.exists():
121
+ graph_dir = candidate
122
+ break
123
+
124
+ if not graph_dir.exists():
125
+ return
126
+
127
+ db_path = graph_dir / "htmlgraph.db"
128
+ db = HtmlGraphDB(str(db_path))
129
+ if db.connection is None:
130
+ return
131
+
132
+ cursor = db.connection.cursor()
133
+ timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
134
+
135
+ # Ensure session exists (required by FK constraint)
136
+ cursor.execute(
137
+ """
138
+ INSERT OR IGNORE INTO sessions (session_id, agent_assigned, created_at, status)
139
+ VALUES (?, ?, ?, ?)
140
+ """,
141
+ (session_id, "orchestrator-hook", timestamp, "active"),
142
+ )
143
+
144
+ # Record the tool event using the actual schema
145
+ # Schema has: event_id, agent_id, event_type, timestamp, tool_name, session_id, etc.
146
+ event_id = str(uuid.uuid4())
147
+ agent_id = "orchestrator-hook" # Identifier for the hook
148
+
149
+ cursor.execute(
150
+ """
151
+ INSERT INTO agent_events (event_id, agent_id, event_type, timestamp, tool_name, session_id)
152
+ VALUES (?, ?, ?, ?, ?, ?)
153
+ """,
154
+ (event_id, agent_id, "tool_call", timestamp, tool_name, session_id),
155
+ )
156
+
157
+ db.connection.commit()
158
+ db.disconnect()
159
+ except Exception:
160
+ # Graceful degradation - don't fail hook on recording error
161
+ pass
162
+
163
+
97
164
  def is_allowed_orchestrator_operation(
98
165
  tool: str, params: dict[str, Any], session_id: str = "unknown"
99
166
  ) -> tuple[bool, str, str]:
@@ -402,6 +469,7 @@ def enforce_orchestrator_mode(
402
469
  # Check if this is a subagent context - subagents have unrestricted tool access
403
470
  if is_subagent_context():
404
471
  return {
472
+ "continue": True,
405
473
  "hookSpecificOutput": {
406
474
  "hookEventName": "PreToolUse",
407
475
  "permissionDecision": "allow",
@@ -425,18 +493,14 @@ def enforce_orchestrator_mode(
425
493
  manager = OrchestratorModeManager(graph_dir)
426
494
 
427
495
  if not manager.is_enabled():
428
- # Mode not active, allow everything
429
- return {
430
- "hookSpecificOutput": {
431
- "hookEventName": "PreToolUse",
432
- "permissionDecision": "allow",
433
- },
434
- }
496
+ # Mode not active, allow everything with no additional output
497
+ return {"continue": True}
435
498
 
436
499
  enforcement_level = manager.get_enforcement_level()
437
500
  except Exception:
438
501
  # If we can't check mode, fail open (allow)
439
502
  return {
503
+ "continue": True,
440
504
  "hookSpecificOutput": {
441
505
  "hookEventName": "PreToolUse",
442
506
  "permissionDecision": "allow",
@@ -467,6 +531,7 @@ def enforce_orchestrator_mode(
467
531
  )
468
532
 
469
533
  return {
534
+ "continue": False,
470
535
  "hookSpecificOutput": {
471
536
  "hookEventName": "PreToolUse",
472
537
  "permissionDecision": "deny",
@@ -491,6 +556,7 @@ def enforce_orchestrator_mode(
491
556
  ):
492
557
  # Provide guidance even when allowing
493
558
  return {
559
+ "continue": True,
494
560
  "hookSpecificOutput": {
495
561
  "hookEventName": "PreToolUse",
496
562
  "permissionDecision": "allow",
@@ -498,6 +564,7 @@ def enforce_orchestrator_mode(
498
564
  },
499
565
  }
500
566
  return {
567
+ "continue": True,
501
568
  "hookSpecificOutput": {
502
569
  "hookEventName": "PreToolUse",
503
570
  "permissionDecision": "allow",
@@ -513,8 +580,8 @@ def enforce_orchestrator_mode(
513
580
  suggestion = create_task_suggestion(tool, params)
514
581
 
515
582
  if enforcement_level == "strict":
516
- # STRICT mode - loud warning with violation count
517
- error_message = (
583
+ # STRICT mode - advisory warning with violation count (does not block)
584
+ warning_message = (
518
585
  f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
519
586
  f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
520
587
  f"Suggested delegation:\n"
@@ -523,23 +590,25 @@ def enforce_orchestrator_mode(
523
590
 
524
591
  # Add circuit breaker warning if approaching threshold
525
592
  if violations >= circuit_breaker_threshold:
526
- error_message += (
593
+ warning_message += (
527
594
  "🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
528
595
  "Reset with: uv run htmlgraph orchestrator reset-violations\n"
529
596
  )
530
597
  elif violations == circuit_breaker_threshold - 1:
531
- error_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
598
+ warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
532
599
 
533
- error_message += (
600
+ warning_message += (
534
601
  "See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
535
602
  "To disable orchestrator mode: uv run htmlgraph orchestrator disable"
536
603
  )
537
604
 
605
+ # Advisory-only: allow operation but provide warning
538
606
  return {
607
+ "continue": True,
539
608
  "hookSpecificOutput": {
540
609
  "hookEventName": "PreToolUse",
541
- "permissionDecision": "deny",
542
- "permissionDecisionReason": error_message,
610
+ "permissionDecision": "allow",
611
+ "additionalContext": warning_message,
543
612
  },
544
613
  }
545
614
  else:
@@ -549,6 +618,7 @@ def enforce_orchestrator_mode(
549
618
  )
550
619
 
551
620
  return {
621
+ "continue": True,
552
622
  "hookSpecificOutput": {
553
623
  "hookEventName": "PreToolUse",
554
624
  "permissionDecision": "allow",
@@ -592,5 +662,9 @@ def main() -> None:
592
662
  # Enforce orchestrator mode with session_id for history lookup
593
663
  response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
594
664
 
665
+ # Record tool event to database for history tracking
666
+ # This allows subsequent calls to detect patterns (e.g., multiple Reads)
667
+ record_tool_event(tool_name, session_id)
668
+
595
669
  # Output JSON response
596
670
  print(json.dumps(response))
@@ -185,7 +185,9 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict[str,
185
185
 
186
186
  {feature_list}
187
187
 
188
- Activity will be attributed to these features based on file patterns and keywords."""
188
+ Activity will be attributed to these features based on file patterns and keywords.
189
+
190
+ **To view all work and progress:** `htmlgraph snapshot --summary`"""
189
191
  output["hookSpecificOutput"]["sessionFeatureContext"] = context_str
190
192
  context.log("info", f"Loaded {len(active_features)} active features")
191
193
 
htmlgraph/models.py CHANGED
@@ -975,10 +975,13 @@ class Session(BaseModel):
975
975
  parent_activity: str | None = None # Parent activity ID
976
976
  nesting_depth: int = 0 # Depth of nesting (0 = top-level)
977
977
 
978
- # Handoff context
978
+ # Handoff context (Phase 2 Feature 3: Cross-Session Continuity)
979
979
  handoff_notes: str | None = None
980
980
  recommended_next: str | None = None
981
981
  blockers: list[str] = Field(default_factory=list)
982
+ recommended_context: list[str] = Field(
983
+ default_factory=list
984
+ ) # File paths to keep context for
982
985
 
983
986
  # High-frequency activity log
984
987
  activity_log: list[ActivityEntry] = Field(default_factory=list)
@@ -1382,7 +1385,12 @@ class Session(BaseModel):
1382
1385
 
1383
1386
  # Build handoff HTML
1384
1387
  handoff_html = ""
1385
- if self.handoff_notes or self.recommended_next or self.blockers:
1388
+ if (
1389
+ self.handoff_notes
1390
+ or self.recommended_next
1391
+ or self.blockers
1392
+ or self.recommended_context
1393
+ ):
1386
1394
  handoff_section = """
1387
1395
  <section data-handoff>
1388
1396
  <h3>Handoff Context</h3>"""
@@ -1405,6 +1413,18 @@ class Session(BaseModel):
1405
1413
  </ul>
1406
1414
  </div>"""
1407
1415
 
1416
+ if self.recommended_context:
1417
+ context_items = "\n ".join(
1418
+ f"<li>{file_path}</li>" for file_path in self.recommended_context
1419
+ )
1420
+ handoff_section += f"""
1421
+ <div data-recommended-context>
1422
+ <strong>Recommended Context:</strong>
1423
+ <ul>
1424
+ {context_items}
1425
+ </ul>
1426
+ </div>"""
1427
+
1408
1428
  handoff_section += "\n </section>"
1409
1429
  handoff_html = handoff_section
1410
1430
 
@@ -9,6 +9,7 @@ from .model_selection import (
9
9
  get_fallback_chain,
10
10
  select_model,
11
11
  )
12
+ from .spawner_event_tracker import SpawnerEventTracker, create_tracker_from_env
12
13
 
13
14
  # Export modular spawners for advanced usage
14
15
  from .spawners import (
@@ -37,6 +38,9 @@ __all__ = [
37
38
  "CodexSpawner",
38
39
  "CopilotSpawner",
39
40
  "ClaudeSpawner",
41
+ # Spawner event tracking
42
+ "SpawnerEventTracker",
43
+ "create_tracker_from_env",
40
44
  # Model selection
41
45
  "ModelSelection",
42
46
  "TaskType",
@@ -22,7 +22,7 @@ class PluginManager:
22
22
  """Get the plugin directory path.
23
23
 
24
24
  Returns:
25
- Path to packages/claude-plugin/.claude-plugin
25
+ Path to packages/claude-plugin (the plugin root, not .claude-plugin)
26
26
  """
27
27
  # Path(__file__) = .../src/python/htmlgraph/orchestration/plugin_manager.py
28
28
  # Need to go up 5 levels to reach project root
@@ -30,7 +30,6 @@ class PluginManager:
30
30
  Path(__file__).parent.parent.parent.parent.parent
31
31
  / "packages"
32
32
  / "claude-plugin"
33
- / ".claude-plugin"
34
33
  )
35
34
 
36
35
  @staticmethod